22.10.2026 - Reading time ~15 Minutes

MikroTik Status dashboard in Signal K

Monitoring the boat network with SignalK

This is a third part of the manual how to use Mikrotik LAMP R16 on the boat.
The second one is about making a dumbest tablet a navigation device.

Let’s be honest: a modern boat without internet is a boat without half of its brains. Weather forecasts and GRIB files, the marina’s WhatsApp, the AIS picture beyond the horizon, chart updates, the bank’s login code, work (yes, some of us work from the boat), and the kids’ cartoons, which are the most mission-critical service on board. When the internet drops, everything is “loading…”, and the whole crew looks at you, because you’re the one who installed “that router”.

And here’s the funny part: the router is the one device on board you can’t just go and look at. LAMP sits on top of the mast or on the pushpit, in its sealed box, with one cable going down. No lights to stare at, no screen, nothing. Is it the signal? Is it the carrier? Is the router alive at all? Did the modem decide to take a nap while still saying it’s connected? It happens, and it’s a true issue: 5G in a busy marina is a lottery, and LTE out at sea is a lottery without tickets.

Disclaimer: there’s no cure for a bad signal. A dashboard won’t give you internet in the middle of the Tyrrhenian Sea. But it will tell you in one glance whether it’s the sea, the carrier or the router, and that’s half of the solution.

So LAMP needs an eye. And the boat most probably already has the right place to put it: Signal K, the open-source boat data server that collects everything from NMEA, sensors and instruments, often running on a Raspberry Pi somewhere under the chart table. If it shows the wind and the depth, it can show the internet too.

What we’re building

LAMP (LTE, GNSS, SMS) ─┐
                       ├──REST, read-only── Signal K plugin ──WebSocket── dashboard on any tablet/phone/laptop
RB751G (Wi-Fi) ────────┘

A small Signal K plugin, signalk-mikrotik, reads every MikroTik router on board through the RouterOS REST API and publishes everything under mikrotik.<router>.*. The same plugin serves a dashboard web page, the one in the picture above. One card per router, built from whatever the router has: no LTE, no LTE section; no Wi-Fi, no Wi-Fi section.

What you see on LAMP’s card:

Section What it tells you
Header RouterOS version, uptime, CPU, RAM, free flash, online badge. And a red Reboot button.
LTE Signal score 1–10 with bars, 4G or 5G, band, RSRP, SINR, RSRQ, how long the session lives. “Connection issues” in red when the internet is gone, a 5G switch and a green LTE Rst button.
SMS How many messages, the latest one. Tap to read all of them.
Checks The router’s netwatch entries, ✓ or ✗.
GNSS Position, fix, satellites, HDOP, speed, course.
Interfaces Traffic in and out with a 10-minute chart.
Devices Who’s connected where.

And on the Wi-Fi router’s card: the Wi-Fi clients with their signal and traffic. That’s how you find out that it’s not the internet that is slow, it’s the kid’s tablet downloading a 40 GB game update.

The useful thing is the combination. “Signal 2/10, SINR −3 dB” means move the boat or wait. “Signal 7/10, Connection issues” means the carrier or the modem is stuck, press LTE Rst. “Card is grey, offline since 14:32” means the router itself is gone, go check the cable. No guessing.

Prerequisites

  • Signal K server 2.x on Node.js 18 or newer.
  • MikroTik routers with RouterOS 7.9 or newer. LAMP from the first part, and any other MikroTik on board.
  • The Signal K host can reach the routers on TCP port 80.
  • For the recovery buttons: the NMEA container from the second part.

Preparing the routers

On each router, create a user that can read everything and change nothing. Replace SK_PASS with a password of letters and digits:

/user group add name=signalk policy=read,test,api,rest-api
/user add name=signalk group=signalk password=SK_PASS

read is for the settings and tables, test is for the LTE and GPS monitor commands. No write, so the dashboard can’t break anything even if it wanted to.

REST lives on the router’s web service, so make sure it’s on:

/ip service set www disabled=no
/ip service print where name=www

If the address column shows addresses, the Signal K host’s network must be among them. For example, LAMP set up as in the first part allows only its own boat network, and a Signal K box behind a second router comes from that router’s address:

/ip service set www address=192.168.188.0/24,10.10.10.0/24

Keep what was already there, add the new one with a comma.

On LAMP, set the GPS to decimal degrees, it’s the least ambiguous format:

/system gps set coordinate-format=dd

Test it from the Signal K host:

curl -u signalk:SK_PASS http://ROUTER_IP/rest/system/resource

JSON with cpu-load and uptime means it works. 401 means a wrong password. A timeout means www is off or doesn’t allow your address.

Note: REST over plain HTTP sends the password in clear text over the boat network. The user is read-only and the boat network is yours, so I can live with that. If you can’t, RouterOS can do HTTPS, but then you get to deal with certificates on a boat. Your choice.

SMS

If you followed the first part, LAMP already receives SMS and keeps the newest 20. Otherwise:

/tool sms set port=lte1 receive-enabled=yes polling=yes

The signalk user can read the inbox, nothing else to do. Why SMS on a dashboard at all? Because the carrier sends “your data plan is over” and “your top-up was successful” to the SIM that lives on the mast, and you’d better see those without climbing.

Installing the plugin

The plugin is three files: package.json, the plugin itself index.js and the dashboard public/index.html. Download them all at once as signalk-mikrotik.tar.gz to the Signal K host and install:

mkdir -p ~/signalk-plugins && cd ~/signalk-plugins
curl -LO https://y-b.one/signalk-mikrotik/signalk-mikrotik.tar.gz
tar xzf signalk-mikrotik.tar.gz
cd ~/.signalk && npm install ~/signalk-plugins/signalk-mikrotik
sudo systemctl restart signalk

No dependencies, nothing downloaded from npm. The plugin uses only what Node.js has built in, and the dashboard is one HTML file without external fonts or scripts, so it works when the internet doesn’t. Which is, you know, exactly when you need it.

Note: if Signal K runs in Docker, ~/.signalk lives in the container’s volume. Copy the folder there and run npm install inside the container.

Configuring it

In the Signal K admin: Server → Plugin Config → MikroTik RouterOS, tick Enabled and add one entry per router:

Field LAMP Wi-Fi router
Device ID in paths lamp rb751
Router IP address 192.168.188.1 10.10.10.1
User / Password signalk / SK_PASS signalk / SK_PASS
Interfaces to report ^(?!lo$) ^(?!lo$)
Recovery trigger URL see below empty

Click Submit. The status line says Polling 2 router(s): lamp, rb751, and the dashboard is at:

http://SIGNALK_IP:3000/signalk-mikrotik/

It’s also listed in the Signal K admin under Webapps as MikroTik Status. Put it on the tablet’s home screen and it’s an app.

What it reads and how often:

Data Every
GNSS 1 s
Interface traffic, Wi-Fi clients, LTE signal 5 s
Devices, system, netwatch, SMS 30 s
Internet check 60 s

That’s about one request per second plus a handful every few seconds. Even the old RB751G with its 400 MHz CPU doesn’t notice.

Little things that matter

  • Themes: the Theme button cycles Auto → Day → Dark → Night, which is red on black. On a night watch you don’t want a white screen burning your night vision. Each tablet remembers its choice.
  • Offline routers don’t disappear: the card goes grey with the last values and offline since HH:MM. So you know when it died, which is often the best hint why.
  • SMS: a new message turns the header button into ✉ SMS · 1 new with a pulsing red dot. Tap it, and a side panel shows all messages, newest first. Each tablet tracks what it has already seen.
  • History: the charts keep 10 minutes in the browser. If Signal K records history (InfluxDB, for example), the charts load it on open.
  • Layout: two cards side by side on a tablet in landscape, one on a phone.

When the internet hangs

Every minute the plugin asks LAMP to ping 1.1.1.1 and 9.9.9.9. One answer from either is enough. Three minutes without any, and Connection issues appears in red next to the LTE heading. It goes away with the first answer.

That’s all it does automatically: it reports. It never restarts anything. I tried the automatic way first, of course, and it was a bad idea. The modem also gives the boat its GPS, and every modem restart costs up to 12 minutes without a position. In a marina that’s annoying. At sea, out of coverage, an automatic watchdog would kill the GPS again and again, every few minutes, exactly when it’s needed for navigation. So the dashboard tells you, and you decide.

To decide something, you need buttons:

  • LTE Rst, green, on the LTE heading. Switches the LTE connection off and on. The internet is back in about 20 seconds, the GPS usually keeps its fix. Try this first.
  • Reboot, red, in the card header. Reboots the whole router: internet, boat network, DNS and NMEA drop for about 2 minutes.
  • 5G, a switch left of LTE Rst, green when on. Switches 5G off and on. Here’s why it exists: with a poor signal, 5G NSA (5G on top of LTE) can keep losing its 5G part, and the session goes dead while the router still says it’s connected. LTE Rst helps for a minute or two, then it dies again. With 5G off, LAMP stays on plain 4G LTE until you switch it back on, even after a reboot. If the hangs stop, you know who’s guilty.

Note: the modem has an AT command to switch 5G off (AT+QNWPREFCFG="nr5g_disable_mode"), but on MikroTik R16 devices RouterOS quietly reverts it within minutes. The switch changes RouterOS’s own network-mode instead, removing or adding 5g, and that one stays.

All of them ask for confirmation, and a second press within 3 minutes is refused, so a nervous double tap doesn’t reboot the router twice.

The trigger on LAMP

The plugin’s signalk user is read-only, and I want it to stay that way. So the buttons don’t talk to the router directly. They call a tiny web trigger running in the busybox container from the second part, on a secret address. It knows these actions:

Address ends with Action
?lte Restart the LTE connection.
?5g-off / ?5g-on Switch 5G off (LTE only) and back on.
?reboot Reboot the router.
nothing Power-cycle the modem, as if you unplugged it for 5 seconds. For when even LTE Rst doesn’t help.

After a 5G switch, an LTE restart or a power-cycle, it also watches the router’s GPS reader for a few minutes and restarts it if it got stuck. Yes, that happens too: after a modem power-cycle RouterOS sometimes just stops reading the GPS until you switch it off and on.

Two more files go next to run.sh from the second part. start.sh starts the trigger and then the NMEA server as before. Edit only CYCLETOKEN, the secret part of the address, at least 20 random letters and digits (openssl rand -hex 12 makes one):

#!/bin/sh
# Starts the modem power-cycle web trigger, then the NMEA server (run.sh).
# The router starts it as: start.sh serve <password> <lte-interface>

# ---- Setting ----
CYCLETOKEN="ChangeMeToALongRandomString" # secret part of the trigger URL, letters and digits only
# -----------------

# Remove Windows line endings, then run the cleaned copy. #
mkdir -p /tmp #
[ -z "$START_CLEAN" ] && tr -d '\r' < "$0" > /tmp/start.sh && export START_CLEAN=1 && exec /bin/sh /tmp/start.sh "$@" #

# The trigger reads the router API address from here. /tmp is inside the container only.
printf 'API=%s\n' "http://nmea:$2@172.17.0.1/rest" > /tmp/cycle.env
mkdir -p /tmp/www/cgi-bin
tr -d '\r' < /nmea/cycle.cgi > "/tmp/www/cgi-bin/$CYCLETOKEN"
chmod 755 "/tmp/www/cgi-bin/$CYCLETOKEN"
httpd -p 8080 -h /tmp/www
exec /bin/sh /nmea/run.sh "$@"

cycle.cgi does the actual work through the router’s REST API, no changes needed:

#!/bin/sh
# Recovery trigger, through the router's REST API. One action per 3 minutes.
#   ?lte     restart the LTE connection (interface off and on)
#   ?reboot  reboot the whole router
#   ?5g-off  switch 5G off (LTE only), ?5g-on switch it back on
#   (none)   power-cycle the LTE modem
# After any LTE change or a power-cycle it restarts the router's GPS reader if it got stuck.
LOCK=/tmp/modem-cycle.last
LTE=lte1
[ -f /tmp/cycle.env ] && . /tmp/cycle.env
[ -n "$API" ] || API=$(sed -n 's/^API="\(http:[^"]*\/rest\).*/\1/p' /nmea/run.sh)

post() { # post <path> <json>
  wget -q -O - --header 'Content-Type: application/json' --post-data "$2" "$API/$1"
}

if [ "$1" = gps ]; then
  # Background part: if the GPS reader gets no data, switch GPS off and on
  # (this also resends the init string to the modem). Up to about 4 minutes.
  for i in 1 2 3 4 5 6 7 8; do
    sleep 20
    case "$(post system/gps/monitor '{"once":""}')" in
      *'"data-age":"never"'*|'') ;;
      *) exit 0 ;;
    esac
    post system/gps/set '{"enabled":"no"}' >/dev/null
    post system/gps/set '{"enabled":"yes"}' >/dev/null
    sleep 10
  done
  exit 1
fi

gps_watch() {
  if command -v setsid >/dev/null; then
    setsid /bin/sh "$0" gps </dev/null >/dev/null 2>&1 &
  else
    /bin/sh "$0" gps </dev/null >/dev/null 2>&1 &
  fi
}

now=$(cut -d. -f1 /proc/uptime)
last=$(cat "$LOCK" 2>/dev/null || echo -1000)
printf 'Content-Type: text/plain\r\n\r\n'
if [ $((now - last)) -lt 180 ]; then
  echo "busy: last action $((now - last)) s ago, try again in $((180 - now + last)) s"
  exit 0
fi
echo "$now" > "$LOCK"

case "$QUERY_STRING" in
  lte)
    if post interface/lte/disable "{\"numbers\":\"$LTE\"}" >/dev/null; then
      sleep 5
      post interface/lte/enable "{\"numbers\":\"$LTE\"}" >/dev/null
      echo "ok: LTE restarted, internet is back in about 20 s"
      gps_watch
    else
      rm -f "$LOCK"
      echo "error: router API call failed"
    fi
    ;;
  5g-off|5g-on)
    # Reads the modem's network-mode and removes or adds only "5g". RouterOS reconnects LTE by itself.
    mode=$(wget -q -O - "$API/interface/lte?name=$LTE" | sed -n 's/.*"network-mode":"\([^"]*\)".*/\1/p')
    if [ -z "$mode" ]; then
      rm -f "$LOCK"
      echo "error: router API call failed"
      exit 0
    fi
    new=$(echo ",$mode," | sed 's/,5g,/,/; s/^,//; s/,$//')
    [ -n "$new" ] || new=lte
    [ "$QUERY_STRING" = 5g-on ] && new="$new,5g"
    if post interface/lte/set "{\"numbers\":\"$LTE\",\"network-mode\":\"$new\"}" >/dev/null; then
      if [ "$QUERY_STRING" = 5g-on ]; then
        echo "ok: 5G on, internet is back in about 20 s"
      else
        echo "ok: 5G off, LTE only until you switch it back on. Internet is back in about 20 s"
      fi
      gps_watch
    else
      rm -f "$LOCK"
      echo "error: router API call failed"
    fi
    ;;
  reboot)
    # The router answers, then reboots. No answer within 10 s: it was refused or is already going down.
    if wget -q -T 10 -O /dev/null --header 'Content-Type: application/json' \
      --post-data '{}' "$API/system/reboot"; then
      echo "ok: router is rebooting, back in about 2 minutes"
    else
      rm -f "$LOCK"
      echo "error: reboot refused or no answer. Does the nmea group have the reboot permission?"
    fi
    ;;
  *)
    if post system/routerboard/usb/power-reset '{"duration":"5s"}' >/dev/null; then
      echo "ok: modem power-cycled, LTE is back in about a minute, GPS fix a few minutes later"
      gps_watch
    else
      rm -f "$LOCK"
      echo "error: router API call failed"
    fi
    ;;
esac

Upload both:

scp start.sh cycle.cgi admin@LAMP_IP:flash/nmea/

Warning: when replacing a file that is already on the router, RouterOS may silently keep the old one. Remove it first with /file remove flash/nmea/cycle.cgi, then upload again, and compare the size in /file print. This one cost me an evening.

Now open port 8080 to the container from the boat network, let the container’s nmea user reboot the router, and stop the container:

/ip firewall nat add chain=dstnat action=dst-nat protocol=tcp dst-port=8080 dst-address-type=local in-interface-list=LAN to-addresses=172.17.0.2 to-ports=8080 comment=modem-cycle
/ip firewall nat move [find comment=modem-cycle] destination=0
/user group set nmea policy=read,write,test,reboot,api,rest-api
/container stop [find interface=veth-nmea]

Wait until /container print shows S. The comment exited with status 143 is fine, that’s how a stopped container says goodbye. Then switch it to start.sh, keeping the password and the modem name from the second part:

:local c [/container get [find interface=veth-nmea] cmd]; /container set [find interface=veth-nmea] cmd=("/bin/sh /nmea/start.sh" . [:pick $c [:find $c " serve"] [:len $c]])
/container start [find interface=veth-nmea]

Test it from a browser on the boat network, the gentlest way:

http://LAMP_IP:8080/cgi-bin/CYCLETOKEN?lte

ok: LTE restarted, internet is back in about 20 s means it works. Put the address without ?lte into the plugin’s Recovery trigger URL, click Submit, and the buttons appear on the dashboard within 30 seconds.

Important: don’t send the trigger address in chat apps. Link previews open it, and your messenger power-cycles your modem. Funny once.

Troubleshooting

Symptom Fix
Plugin not in Plugin Config Not installed or the server not restarted. Check the server log for signalk-mikrotik.
Log: HTTP 401 Wrong user or password. Test with curl.
Log: fetch failed or TimeoutError Router unreachable, or www off or not allowing the Signal K host.
No LTE values The signalk group lacks test.
No GNSS values GPS off on the router: /system gps print.
Position off by hundreds of km coordinate-format=dd on the router.
Dashboard says “Waiting for data” Plugin disabled, or no router answers.
No 5G / LTE Rst / Reboot buttons No Recovery trigger URL in the plugin settings.
A button says error: trigger unreachable The container isn’t running, or the URL is wrong. Open it from a laptop.
A button says reboot refused Run the user group set nmea line above.
A button says not allowed Signal K security is on: log in to Signal K in the same browser.
SMS text is garbled Unicode SMS. RouterOS reads plain GSM text only. Not fixable.

Some useful notes

  • The position stays under mikrotik.lamp.gnss.* and is not written to the boat’s navigation.position on purpose. If you have a better GPS, like an AIS transponder’s antenna, it shouldn’t have to compete with a router.
  • SMS texts are in the Signal K data, bank codes included. Anyone who can open Signal K can read them. On a family boat that’s fine. On a charter boat, think twice.
  • The signalk user can read the whole router configuration. It can’t change anything, but keep the boat network for the boat.
  • The plugin never deletes or sends SMS. The sms-keep-20 script from the first part does the cleaning.

Now, when the crew asks “why is the internet not working?”, you open the tablet, look at one card, and say something smart like “SINR is minus three, we’re behind the breakwater”. They won’t understand a word, but they’ll trust you. Hooraaah!