Why This Architecture Works

NAT traversal solved — devices initiate all connections outward

The NAT Problem: Hotel MikroTik devices sit behind the hotel's own router/NAT. Your AWS cloud server has no way to directly dial into 192.168.x.x on-premise devices. The solution: devices always initiate connections outward to cloud — REST API calls and MQTT subscriptions both go outbound on port 443/8883 which any firewall allows.
Config via REST API
Device calls GET /api/v1/devices/{mac}/config with Bearer token. Runs on boot, daily at 03:00 AM, or instantly via MQTT provision command. No polling — config only applied when hash changes.
Authenticated + versioned
MQTT Commands (RouterOS Native)
RouterOS v7.1+ has built-in MQTT client. Device subscribes to cmd/{mac}/# on your self-hosted EMQX broker. Cloud publishes commands (reboot, PoE toggle, provision, kick-guest) — device receives instantly and executes.
Native RouterOS support
Status Reporting (MQTT Publish)
Device publishes CPU, RAM, uptime, port states, PoE watts, DHCP leases, active hotspot sessions to status/{mac} every 60 seconds. All changes logged to audit trail.
Real-time telemetry
 When Config is Pulled
On Boot
30 seconds after device powers on. Ensures fresh config after any outage or maintenance.
Daily at 03:00 AM
Nightly refresh picks up any config changes made during the day. Low-traffic window.
On MQTT Command
Dashboard sends provision command. Device fetches + applies immediately. Use after config changes.
1

Full System Architecture

Cloud components, MQTT broker, and on-premise MikroTik devices

Cloud / Self-hosted
S3 Bucket / Static Config
JSON config files per device MAC
HTTPS GET JSON
EMQX MQTT Broker (self-hosted)
Port 1883 (plain) or 8883 (TLS) — username/password auth
MQTT 3.1.1 User + Pass
EMQX Rule Engine / Webhook
Processes status/ topics → forwards to DB or API
EMQX Rules Webhook
HotelNet Dashboard
SA portal publishes commands via API → EMQX broker
REST API Publish
HTTPS GET
Config Pull
MQTT CMD
Commands
MQTT STATUS
Telemetry
Hotel On-Premise
MikroTik Gateway (hEX / RB4011)
RouterOS v7 — DHCP, NAT, HotSpot, Firewall
MQTT Client HTTPS Fetch REST API
CRS3xx Switch (RouterOS)
Full RouterOS — PoE, VLANs, full MQTT support
MQTT Client HTTPS Fetch SNMP
CSS3xx Switch (SwOS)
SwOS only — No MQTT. Config via Gateway proxy script
SNMP HTTP JSON
Access Points (cAP / hAP)
RouterOS — managed via Gateway CAPsMAN
CAPsMAN
CSS3xx Exception: CSS326/CSS610 run SwOS which has no MQTT client. Solution: The MikroTik Gateway (which does have MQTT) acts as a proxy — it receives the MQTT command, then uses /tool/fetch to call the SwOS HTTP API on the local network (192.168.x.x) and applies the config. From cloud's perspective, all commands go through the same MQTT channel.
2

Auto-Provisioning Flow

Step-by-step: device boots → downloads config → applies → reports back

 Phase 1: Config Pull (HTTPS)
1
Device Boots / Scheduler Fires
RouterOS scheduler runs every 5 minutes. Triggers provision-check script. No cloud involvement needed.
2
Call REST API with Bearer Token
Script uses /tool/fetch with Authorization: Bearer {token} header to GET full config JSON from the API. Token stored securely in RouterOS environment variables.
# RouterOS script (provision-check.rsc) :local mac [/interface get ether1 mac-address] :local token [/system/script/environment/get device_token value-name=value] /tool/fetch url=("https://api.hotelnet.io/v1/devices/".$mac."/config") \ http-method=get \ http-header-field=("Authorization: Bearer ".$token) \ dst-path=hotelnet-config.json
3
Parse & Apply JSON Config
Script reads downloaded JSON, extracts VLANs, SSIDs, firewall rules, PoE settings, bandwidth limits and applies them using RouterOS commands.
4
Report Provision Result
Device publishes result to MQTT topic provision/{mac}/result with success/error status and applied config hash.
 Phase 2: MQTT Command Channel
1
Device Subscribes on Boot
On startup, RouterOS connects to your EMQX broker using username & password and subscribes to cmd/{mac}/#. Persistent session — commands queue while offline.
2
Dashboard Sends Command
SA clicks "Reboot AP" or "Toggle PoE Port 3". Dashboard calls REST API → publishes to EMQX broker MQTT topic via HTTP API or direct MQTT.
3
RouterOS Receives & Executes
MQTT on-message script fires, parses JSON payload, routes to correct handler (reboot, poe-toggle, vlan-change, provision-now, etc.).
# on-message handler :if ($cmd = "poe-toggle") do={ /interface/poe set [ find name=$port] poe-out=$state }
4
Publish Execution Result
Device publishes cmd/{mac}/ack with result JSON. Dashboard shows real-time success/failure notification.
3

Device Config JSON — REST API Response

Returned by GET /api/v1/devices/{mac}/config — full RouterOS configuration per device type

The flat object contains pre-computed simple key→value pairs for easy RouterOS string parsing (RouterOS has no native JSON parser). The rest of the JSON is the full human-readable config for dashboard display and history tracking.
// REST API: GET https://api.hotelnet.io/v1/devices/AA:BB:CC:DD:EE:FF/config
// Header:   Authorization: Bearer {device_token}
{
  "schema_version" 2
  "config_version" "20260520-143022"
  "config_hash" "sha256:abc123def456"  // device skips apply if unchanged
  "force_apply" false
  "device_type" "gateway"           // gateway | switch-crs | switch-css
  "device"
    "mac" "AA:BB:CC:DD:EE:FF"
    "property_id" "prop-001"
    "property_name" "Grand Palace Hotel"
  

  // ── FLAT section: pre-computed key=value for RouterOS string parsing ──
  "flat"
    "hostname"                  "GW-GRANDPALACE-01"
    "timezone"                  "Asia/Kolkata"
    "ntp_primary"               "time.cloudflare.com"
    "ntp_secondary"             "time.google.com"
    "dns1"                      "1.1.1.1"
    "dns2"                      "8.8.8.8"
    "snmp_community"            "hotelnet-gp01"
    "mgmt_password"             "unchanged"   // "unchanged" = skip
    "vlan_guest_id"             "10"
    "vlan_staff_id"             "20"
    "vlan_mgmt_id"              "99"
    "dhcp_guest_pool"           "10.10.10.100-10.10.10.240"
    "dhcp_guest_lease"          "12h"
    "dhcp_staff_pool"           "10.20.20.50-10.20.20.100"
    "hotspot_login_page"        "https://portal.hotelnet.io/login?prop=prop-001"
    "hotspot_session_timeout"   "24h"
    "hotspot_idle_timeout"      "30m"
    "hotspot_keepalive_timeout" "2m"
    "hotspot_shared_users"      "2"
    "hotspot_mac_auth"          false
    "bw_guest_up"               "10M"
    "bw_guest_down"             "20M"
    "bw_staff_up"               "50M"
    "bw_staff_down"             "100M"
    "ssid_guest"                "GRAND-PALACE-WIFI"
    "ssid_staff"                "GP-STAFF-ONLY"
    "wifi_5g_channel"           "149"
    "mqtt_broker"               "mqtt.hotelnet.io"
    "mqtt_port"                 "1883"
    "mqtt_user"                 "device-AA-BB-CC-DD-EE-FF"
    "mqtt_password"             "EMQX_DEVICE_PASSWORD"
  

  // ── Full structured config (for dashboard display + audit diff) ──
  "network"
    "wan" "interface" "ether1" "type" "dhcp" 
    "vlans"
      "id"10"name""GUEST""subnet""10.10.10.0/24""gateway""10.10.10.1"
      "id"20"name""STAFF""subnet""10.20.20.0/24""gateway""10.20.20.1"
      "id"99"name""MGMT""subnet""192.168.100.0/24""gateway""192.168.100.1"
    
    "firewall" "block_inter_vlan"true "block_guest_mgmt"true 
  
  "hotspot" "enabled"true "interface""vlan10" 
  "capsman" "enabled"true "channel_width_5g""80MHz" "tx_power""20" 
  "mqtt" "broker""mqtt.hotelnet.io" "port"1883 "qos"1 "tls"false "status_interval_sec"60 
}
{
  "schema_version" 2
  "device"
    "mac" "11:22:33:44:55:66"
    "type" "switch-crs"
    "model" "CRS354-48P-4S+"
    "property_id" "prop-001"
    "hostname" "SW-GRANDPALACE-01"
  
  "vlans"
     "id" 10 "name" "GUEST" "tagged_ports" "ether1" "untagged_ports" "ether3""ether4" 
     "id" 20 "name" "STAFF" "tagged_ports" "ether1" "untagged_ports" "ether5""ether6" 
  
  "poe_ports"
     "port" "ether3" "poe_out" "auto-on" "priority" "high" "name" "AP-Floor1" 
     "port" "ether4" "poe_out" "auto-on" "priority" "high" "name" "AP-Floor2" 
     "port" "ether5" "poe_out" "forced-on" "priority" "low" "name" "Camera-Lobby" 
  
  "mqtt"
    "broker" "mqtt.hotelnet.io"
    "port" 1883
    "username" "device-SW-112233445566"
    "client_id" "SW-112233445566"
    "cmd_topic" "cmd/11:22:33:44:55:66/#"
    "status_topic" "status/11:22:33:44:55:66"
    "status_interval_sec" 60
  
}
// Config file URL pattern hosted on S3:
// https://config.hotelnet.io/devices/{MAC_ADDRESS}.json
// https://config.hotelnet.io/devices/AA:BB:CC:DD:EE:FF.json

// Config file also has a version hash for change detection:
{
  "schema_version" 2
  "config_version" "20260520-143022"  // timestamp of last change
  "config_hash" "sha256:abc123..."     // device skips apply if hash matches
  "force_apply" false               // set true to force re-apply even if same hash
  "device"
  // ... rest of config
}

// S3 bucket structure:
// s3://hotelnet-device-configs/
//   devices/
//     AA:BB:CC:DD:EE:FF.json   ← gateway
//     11:22:33:44:55:66.json   ← switch
//   templates/
//     gateway-template.json    ← base template
//     crs-switch-template.json
//   history/
//     AA:BB:CC:DD:EE:FF/       ← versioned backups
//       20260520-143022.json
4

MQTT Topic Design

Complete topic structure for commands, status, provisioning, and events

Topic Pattern Direction QoS Publisher Subscriber Description
cmd/{mac}/provision → Device 1 Dashboard / API RouterOS Trigger immediate config pull + apply
cmd/{mac}/reboot → Device 1 Dashboard / API RouterOS Reboot gateway or switch
cmd/{mac}/poe-toggle → Device 1 Dashboard / API RouterOS Payload: {"port":"ether3","state":"off"}
cmd/{mac}/poe-status → Device 0 Dashboard RouterOS Request instant PoE readings for all ports
cmd/{mac}/kick-guest → Device 1 Dashboard RouterOS Payload: {"session_id":"xxx","mac":"..."}
cmd/{mac}/vlan-change → Device 1 Dashboard RouterOS Change port VLAN assignment remotely
cmd/{mac}/bw-limit → Device 1 Dashboard RouterOS Apply new bandwidth limit to specific user/IP
status/{mac} ← Cloud 0 RouterOS Lambda / Dashboard Full device telemetry every 60s (CPU, RAM, ports, uptime)
status/{mac}/poe ← Cloud 0 RouterOS Lambda / Dashboard PoE per-port watts, current, voltage
status/{mac}/sessions ← Cloud 0 RouterOS Lambda / Dashboard Active hotspot sessions list
events/{mac} ← Cloud 1 RouterOS Lambda / Alerting Link down, PoE overload, high CPU, auth failures
cmd/{mac}/ack ← Cloud 1 RouterOS Dashboard Command execution result / acknowledgement
provision/{mac}/result ← Cloud 1 RouterOS Lambda / Dashboard Config pull result: applied hash, timestamp, errors
5

RouterOS Scripts

Paste these scripts directly into MikroTik RouterOS — no extra software needed

# ============================================================
# provision-check.rsc
# Scheduled every 5 minutes via /system/scheduler
# Downloads device config JSON from cloud and applies it
# ============================================================

# Get device MAC for URL construction
:local mac [/interface get [find name="ether1"] mac-address]
:local configUrl ("https://config.hotelnet.io/devices/" . $mac . ".json")
:local localFile "provision.json"
:local currentHash ""

# Read current config hash from variable (stored after last apply)
:if ([/system/script/environment/find name="cfg_hash"] != "") do={
  :set currentHash [/system/script/environment/get cfg_hash value-name=value]
}

# Fetch config file from S3
:do {
  /tool/fetch url=$configUrl dst-path=$localFile
  :log info ("Provision: downloaded config from " . $configUrl)
} on-error={
  :log error "Provision: FAILED to download config file"
  /mqtt/publish broker="hotelnet" topic=("provision/".$mac."/result") \
    message=("{\"status\":\"error\",\"reason\":\"fetch_failed\"}")
  :error "fetch failed"
}

# Parse JSON — extract config_hash and force_apply
:local configData [/file/get $localFile contents]
:local newHash ""

# Simple hash extraction (RouterOS string search)
:local hashStart ([:find $configData "\"config_hash\":\""] + 15)
:local hashEnd [:find $configData "\"" $hashStart]
:set newHash [:pick $configData $hashStart $hashEnd]

# Skip apply if config hasn't changed (unless force_apply=true)
:if ($newHash = $currentHash) do={
  :log info "Provision: config unchanged, skipping apply"
  :error "no change"
}

# Config changed — call the apply-config script
:log info ("Provision: applying new config hash " . $newHash)
/system/script/run apply-config

# Save new hash
:if ([/system/script/environment/find name="cfg_hash"] = "") do={
  /system/script/environment/add name="cfg_hash" value=$newHash
} else={
  /system/script/environment/set cfg_hash value=$newHash
}

# Publish success result via MQTT
/mqtt/publish broker="hotelnet" \
  topic=("provision/" . $mac . "/result") \
  message=("{\"status\":\"applied\",\"hash\":\"" . $newHash . "\",\"ts\":" . [:tonum [/system/clock/get time]] . "}")
# ============================================================
# mqtt-subscribe.rsc
# Run once on boot via /system/scheduler startup=yes
# Sets up MQTT broker connection and topic subscription
# ============================================================

:local mac [/interface get [find name="ether1"] mac-address]
:local clientId ("ROS-" . [:pick $mac 9 17])  # last 8 chars of MAC

# Remove existing broker config (clean setup)
/iot/mqtt/brokers/remove [find name="hotelnet"]

# Add EMQX broker — username/password auth (no certificates needed)
# Read credentials stored by initial-setup.rsc
:local mqttBroker   [/system/script/environment/get [find name="mqtt_broker"]   value]
:local mqttPort     [/system/script/environment/get [find name="mqtt_port"]     value]
:local mqttUser     [/system/script/environment/get [find name="mqtt_user"]     value]
:local mqttPassword [/system/script/environment/get [find name="mqtt_password"] value]

/iot/mqtt/brokers/remove [find name="hotelnet"]
/iot/mqtt/brokers/add \
  name="hotelnet" \
  address=$mqttBroker \
  port=$mqttPort \
  client-id=$clientId \
  username=$mqttUser \
  password=$mqttPassword \
  ssl=no

# Connect to broker
/iot/mqtt/brokers/connect hotelnet
:delay 3s

# Subscribe to command topics for this device
/iot/mqtt/subscribe \
  broker="hotelnet" \
  topic=("cmd/" . $mac . "/#") \
  qos=1 \
  on-message=cmd-handler

# Subscribe to global broadcast commands
/iot/mqtt/subscribe \
  broker="hotelnet" \
  topic=("cmd/broadcast/#") \
  qos=0 \
  on-message=cmd-handler

:log info ("MQTT: subscribed to cmd/" . $mac . "/#")
# ============================================================
# cmd-handler.rsc
# Called by MQTT on-message event
# Variables: $topic, $message (JSON string)
# ============================================================

:local mac [/interface get [find name="ether1"] mac-address]

# Extract command from topic (last segment after final /)
:local topicLen [:len $topic]
:local lastSlash 0
:for i from=0 to=($topicLen - 1) do={
  :if ([:pick $topic $i ($i+1)] = "/") do={ :set lastSlash $i }
}
:local cmd [:pick $topic ($lastSlash + 1) $topicLen]

:log info ("CMD received: " . $cmd . " | payload: " . $message)

# ── REBOOT ──────────────────────────────────
:if ($cmd = "reboot") do={
  /mqtt/publish broker="hotelnet" topic=("cmd/".$mac."/ack") \
    message=("{\"cmd\":\"reboot\",\"status\":\"executing\"}")
  :delay 2s
  /system/reboot
}

# ── POE TOGGLE ──────────────────────────────
:if ($cmd = "poe-toggle") do={
  # Parse port and state from JSON payload
  :local portStart ([:find $message "\"port\":\""] + 8)
  :local portEnd [:find $message "\"" $portStart]
  :local port [:pick $message $portStart $portEnd]

  :local stateStart ([:find $message "\"state\":\""] + 9)
  :local stateEnd [:find $message "\"" $stateStart]
  :local state [:pick $message $stateStart $stateEnd]

  :do {
    /interface/poe/set [find name=$port] poe-out=$state
    /mqtt/publish broker="hotelnet" topic=("cmd/".$mac."/ack") \
      message=("{\"cmd\":\"poe-toggle\",\"port\":\"".$port."\",\"state\":\"".$state."\",\"status\":\"ok\"}")
    :log info ("PoE " . $port . " set to " . $state)
  } on-error={
    /mqtt/publish broker="hotelnet" topic=("cmd/".$mac."/ack") \
      message=("{\"cmd\":\"poe-toggle\",\"status\":\"error\",\"port\":\"".$port."\"}")
  }
}

# ── POE STATUS REQUEST ───────────────────────
:if ($cmd = "poe-status") do={
  :local poeJson "["
  :local first true
  :foreach iface in=[/interface/poe/find] do={
    :local name [/interface/poe/get $iface name]
    :local watts [/interface/poe/monitor $iface once as-value]
    :if (!$first) do={ :set poeJson ($poeJson . ",") }
    :set poeJson ($poeJson . "{\"port\":\"".$name."\",\"watts\":".(($watts->"poe-out-power"))."}")
    :set first false
  }
  :set poeJson ($poeJson . "]")
  /mqtt/publish broker="hotelnet" topic=("status/".$mac."/poe") message=$poeJson
}

# ── PROVISION NOW ───────────────────────────
:if ($cmd = "provision") do={
  /mqtt/publish broker="hotelnet" topic=("cmd/".$mac."/ack") \
    message=("{\"cmd\":\"provision\",\"status\":\"starting\"}")
  /system/script/run provision-check
}

# ── KICK GUEST ──────────────────────────────
:if ($cmd = "kick-guest") do={
  :local sidStart ([:find $message "\"session_id\":\""] + 14)
  :local sidEnd [:find $message "\"" $sidStart]
  :local sid [:pick $message $sidStart $sidEnd]
  :do {
    /ip/hotspot/active/remove [find .id=$sid]
    /mqtt/publish broker="hotelnet" topic=("cmd/".$mac."/ack") \
      message=("{\"cmd\":\"kick-guest\",\"session_id\":\"".$sid."\",\"status\":\"ok\"}")
  } on-error={
    /mqtt/publish broker="hotelnet" topic=("cmd/".$mac."/ack") \
      message=("{\"cmd\":\"kick-guest\",\"status\":\"error\"}")
  }
}
# ============================================================
# status-publish.rsc
# Scheduled every 60 seconds
# Publishes full device telemetry to MQTT status topic
# ============================================================

:local mac [/interface get [find name="ether1"] mac-address]

# System resources
:local res [/system/resource/get]
:local cpu ($res->"cpu-load")
:local freeMem ($res->"free-memory")
:local totalMem ($res->"total-memory")
:local uptime ($res->"uptime")
:local version ($res->"version")

# Active hotspot sessions count
:local sessionCount [:len [/ip/hotspot/active/find]]

# DHCP leases count
:local leaseCount [:len [/ip/dhcp-server/lease/find status=bound]]

# WAN interface status
:local wanUp [/interface/get ether1 running]

# Build JSON payload
:local payload ("{")
:set payload ($payload . "\"mac\":\"".$mac."\"")
:set payload ($payload . ",\"cpu\":".$cpu)
:set payload ($payload . ",\"mem_free\":".$freeMem)
:set payload ($payload . ",\"mem_total\":".$totalMem)
:set payload ($payload . ",\"uptime\":\"".$uptime."\"")
:set payload ($payload . ",\"version\":\"".$version."\"")
:set payload ($payload . ",\"wan_up\":".$wanUp)
:set payload ($payload . ",\"hotspot_sessions\":".$sessionCount)
:set payload ($payload . ",\"dhcp_leases\":".$leaseCount)
:set payload ($payload . ",\"ts\":".[/system/clock/get time])
:set payload ($payload . "}")

# Publish to MQTT status topic
/iot/mqtt/publish broker="hotelnet" \
  topic=("status/" . $mac) \
  message=$payload \
  qos=0

:log debug ("Status published for " . $mac)
# ============================================================
# initial-setup.rsc
# Run ONCE when installing device at new property
# Creates all scripts and schedulers
# ============================================================

# Upload .rsc files via Winbox Files, SCP, or FTP:
# No certificates needed — EMQX uses username/password auth!
# Just upload the .rsc script files.

# Store EMQX credentials as environment variables
/system/script/environment/add name="mqtt_broker"   value="mqtt.hotelnet.io"
/system/script/environment/add name="mqtt_port"     value=1883
/system/script/environment/add name="mqtt_user"     value="PASTE_MQTT_USERNAME_HERE"
/system/script/environment/add name="mqtt_password" value="PASTE_MQTT_PASSWORD_HERE"
/system/script/environment/add name="mqtt_use_tls"  value=false

# Create scripts (content loaded from individual .rsc files)
/system/script/add name="provision-check"  source=[/file/get provision-check.rsc contents]
/system/script/add name="apply-config"     source=[/file/get apply-config.rsc contents]
/system/script/add name="mqtt-subscribe"   source=[/file/get mqtt-subscribe.rsc contents]
/system/script/add name="cmd-handler"      source=[/file/get cmd-handler.rsc contents]
/system/script/add name="status-publish"   source=[/file/get status-publish.rsc contents]

# Create schedulers
/system/scheduler/add \
  name="mqtt-connect" \
  start-time=startup \
  on-event="/system/script/run mqtt-subscribe" \
  comment="Connect to MQTT broker on boot"

/system/scheduler/add \
  name="provision-check" \
  interval=5m \
  on-event="/system/script/run provision-check" \
  comment="Poll config from cloud every 5 minutes"

/system/scheduler/add \
  name="status-publish" \
  interval=1m \
  on-event="/system/script/run status-publish" \
  comment="Publish telemetry to cloud every 60 seconds"

:log info "Initial setup complete — device registered"
:put "Setup complete! Rebooting to start MQTT connection..."
:delay 3s
/system/reboot
6

Available MQTT Commands

All commands you can send from the dashboard to any RouterOS device

provision
Trigger immediate config pull from S3 — applies latest JSON config right now
{"force": true}
reboot
Gracefully reboot the MikroTik device. Sends ACK before rebooting.
{"delay_sec": 5}
poe-toggle
Turn PoE power on/off for a specific port. Instantly cycles connected AP/camera.
{"port":"ether3","state":"off"}
poe-status
Request real-time PoE readings — watts, current, voltage for all ports.
{}
kick-guest
Terminate a specific hotspot session. User must re-authenticate at portal.
{"session_id":"...","mac":"aa:bb:..."}
bw-limit
Apply or change bandwidth limit queue for a specific IP address.
{"ip":"10.10.10.101","up":"5M","down":"10M"}
vlan-change
Move a port to a different VLAN without full re-provision.
{"port":"ether5","vlan":20}
hotspot-add-user
Create a new hotspot user / voucher directly on the device.
{"user":"guest123","pass":"xxxx","limit-uptime":"24h"}
get-status
Request an immediate full status publish — don't wait for 60s interval.
{}
7

CSS3xx Switch — Gateway Proxy Pattern

How to control SwOS switches that have no MQTT support

How the Proxy Works
Cloud sends MQTT command to Gateway MAC, not the CSS3xx MAC. The gateway script detects the command is for a downstream CSS switch and uses /tool/fetch to call the SwOS HTTP API on the local network.
Payload example — CSS PoE toggle via Gateway:
{
  "target": "css3xx",
  "css_ip": "192.168.100.20",
  "cmd": "poe-toggle",
  "port": 3,
  "state": "off"
}
Gateway Proxy Script
# In cmd-handler.rsc — CSS3xx proxy section :if (($payload->"target") = "css3xx") do={ :local cssIp ($payload->"css_ip") :local cssPort ($payload->"port") :local cssState ($payload->"state") # SwOS PoE API endpoint :local cssUrl ("http://".$cssIp."/poe.b") :local cssBody ("poe[".$cssPort."]=off") /tool/fetch url=$cssUrl http-method=post \ http-data=$cssBody \ http-header-field="Content-Type: application/x-www-form-urlencoded" /mqtt/publish broker="hotelnet" \ topic=("cmd/".$mac."/ack") \ message=("{\"cmd\":\"css-poe\",\"port\":".$cssPort.",\"status\":\"ok\"}") }
8

Security Model

EMQX username/password auth, per-device ACL topic isolation, command signing

Per-Device EMQX Credentials
Each MikroTik device has its own EMQX username (e.g. device-AA-BB-CC-DD-EE-FF) and a unique password. Created in EMQX Dashboard → Access Control → Authentication. No certificates required. Revoke instantly by deleting or disabling the EMQX user.
EMQX ACL — Topic Isolation
EMQX ACL rules restrict each device username to only its own MAC-prefixed topics. Example: user device-AA-BB can only pub/sub to cmd/AA:BB:.../# and status/AA:BB:.../#. Device AA:BB can't read commands for CC:DD. Configure in EMQX Dashboard → Access Control → Authorization.
Optional TLS (port 8883)
For internet-facing EMQX deployments, enable TLS on port 8883. Upload a single CA cert (emqx-ca.pem) to MikroTik — one file for all devices, not per-device certs. For internal/trusted networks port 1883 (plain TCP) is simpler and equally safe inside your infrastructure.
Command Signing (Optional)
Each MQTT command payload can include an HMAC-SHA256 signature using a device-shared secret. RouterOS script validates signature before executing — prevents replay attacks even if EMQX credentials are compromised.
Zero inbound firewall rules needed. All communication is outbound from the hotel device. Port 1883 (MQTT plain) or 8883 (MQTT TLS) and 443 (HTTPS config pull) are standard outbound ports. No port forwarding, no VPN, no static IP required at the hotel. EMQX is your own server — no third-party cloud dependency.
9

What Needs to Be Built

Components required to complete this auto-provisioning system

 Server Side (EMQX + API)
 On-Premise Side (RouterOS)
10

Step-by-Step Installation Guide

Complete technician guide — from unboxing a MikroTik to fully provisioned + MQTT-connected

Time estimate: 15–20 minutes per device for a first-time setup. With EMQX there are no per-device certificates to generate or upload — just a username + password created in the EMQX dashboard once. After you have the template ready, onboarding a new device takes under 10 minutes. Only the device_token, mqtt_user, and mqtt_password differ per device.
1
Prerequisites — What You Need Before Starting
Hardware, software, and access requirements
~5 min
MikroTik Device (RouterOS v7.1+)
Required for built-in MQTT client (/iot/mqtt/). Supported models: RB4011iGS+, hEX S, hEX, RB5009, CCR2004, CRS317, CRS354. Check version: /system/package/print
EMQX Credentials from HotelNet Platform
Get from the device record in the platform: MQTT username (e.g. device-AA-BB-CC-DD-EE-FF), MQTT password, and MQTT broker URL. One set per device. No certificate files needed!
Device Token from HotelNet Platform
A unique Bearer token generated when you register the device MAC in the platform. Found in the device record under device_token field. Looks like: hnm_dev_tok_XXXX_xxxx
Laptop + Winbox (or SSH access)
Winbox 3.x or 4.x for Windows/Mac/Linux. Download from mt.lv/winbox. Alternatively use WebFig (browser) or SSH. Winbox is recommended for certificate upload.
Ethernet Cable to MikroTik ether2
Connect laptop to ether2 (not ether1). ether1 is WAN. Default MikroTik IP is 192.168.88.1. Default login: admin / no password.
WAN Internet Access on ether1
The device needs live internet via ether1 (hotel's ISP cable / PPPoE / DHCP) to download config from the API and connect to EMQX. Test with: /tool/ping 8.8.8.8 count=3
RouterOS License (if not already active)
IoT/MQTT package requires RouterOS v7 and an active license. CHR (cloud-hosted) requires Level 4+. Physical MikroTik hardware ships with license included. Check: /system/license/print
HotelNet Script Package (5 .rsc files)
Download all 5 scripts from the platform or copy from Section 5 of this page: provision-check.rsc, apply-config.rsc, mqtt-subscribe.rsc, cmd-handler.rsc, status-publish.rsc, initial-setup.rsc
Factory reset first? If the device was previously used, do a factory reset before HotelNet installation to avoid config conflicts: In Winbox go to System → Reset Configuration → check "No Default Config" → Apply. Or via terminal: /system/reset-configuration no-defaults=yes
2
Register the Device in HotelNet Platform + Get EMQX Credentials
Create device record, create EMQX user, copy username/password and device token
~5 min (cloud)
In the HotelNet Dashboard
STEP A Go to Super Admin → Devices tab and click + Add Device
STEP B Enter the device MAC address (from the label on the bottom of the MikroTik), select Property, choose Model and Role (gateway/switch-crs)
STEP C Platform creates the device record and shows a device_token. In EMQX Dashboard → Access Control → Authentication, add a new user: username = device-AA-BB-CC-DD-EE-FF, set a strong password
STEP D Note down: device_token, mqtt_broker URL (e.g. mqtt.hotelnet.io), mqtt_user, mqtt_password, and mqtt_port (1883 or 8883)
STEP E Update the Config JSON for this device with property-specific settings (SSIDs, VLANs, PoE budget, bandwidth limits) — see Section 3
Files You'll Have After Download
hotelnet-device-AA-BB-CC-DD-EE-FF/
  ├── device-info.txt  ← token + MAC + MQTT URL + user + pass
  ├── initial-setup.rsc  ← pre-filled with your credentials
  ├── provision-check.rsc
  ├── apply-config.rsc
  ├── mqtt-subscribe.rsc  ← reads creds from env vars
  ├── cmd-handler.rsc
  └── status-publish.rsc
No certificate files needed! Just 6 script files. EMQX credentials (username + password) are stored as RouterOS environment variables by initial-setup.rsc — not hardcoded in any file.
3
Upload Script Files to MikroTik
Get the 6 RSC scripts onto the device filesystem — 3 methods available. No certificate files needed!
~5 min

Upload all 6 script files to the MikroTik's root filesystem. No certificate files required with EMQX — just the .rsc scripts. Choose the method that works best for your setup:

Recommended method. Winbox has a built-in drag-and-drop file manager. Works on Windows, macOS, and Linux (Wine).
  1. Connect laptop to MikroTik ether2 with an Ethernet cable
  2. Open Winbox → click Neighbors tab → double-click your MikroTik → Login as admin
  3. In Winbox menu: Files → the File Manager window opens
  4. Drag and drop all 6 script files from your downloaded package into the Winbox Files window:
    provision-check.rsc
    apply-config.rsc
    mqtt-subscribe.rsc
    cmd-handler.rsc
    status-publish.rsc
    initial-setup.rsc
  5. Wait for all uploads to complete (green progress bars)
  6. Verify files appeared: in Winbox Files, you should see all 6 .rsc files listed
You'll see the file names listed under / (root) in the Winbox Files panel. If certificate files show as 0 bytes, re-upload them.
SCP requires SSH to be enabled on the MikroTik. Enable it in IP → Services → SSH (port 22). Works great from Linux/macOS terminal or Windows PowerShell/WSL.

First, enable SSH if not already enabled:

/ip/service/enable ssh

Then from your laptop terminal, upload all files at once:

# Replace 192.168.88.1 with your MikroTik's IP
# Replace admin with your username

# Upload all 6 script files (no certificate files needed!)
scp provision-check.rsc apply-config.rsc mqtt-subscribe.rsc \
    cmd-handler.rsc status-publish.rsc initial-setup.rsc \
    admin@192.168.88.1:/

# Verify files are there
ssh admin@192.168.88.1 "/file/print"
RouterOS SSH uses a non-standard path. Files go to root (/) which maps to RouterOS filesystem root. Do NOT specify subdirectories — RouterOS scripts must be in the root.
RouterOS has a built-in FTP server. Enable it in IP → Services → FTP. Use any FTP client (FileZilla, WinSCP) or command-line ftp.

Enable FTP service if not active:

/ip/service/enable ftp

Connect with FileZilla (or any FTP client):

Host:     192.168.88.1   (your MikroTik IP)
Protocol: FTP - File Transfer Protocol
User:     admin
Password: (your password)
Port:     21

Drag all certificate and script files to the root directory (/) in FileZilla. After transfer, disable FTP for security:

/ip/service/disable ftp
If the MikroTik already has internet access, you can download all files directly from a hosting URL using RouterOS's built-in /tool/fetch. Ideal for remote zero-touch setups.

Host your files on HTTPS (S3, GitHub, CDN) then run in RouterOS terminal:

# Run in RouterOS New Terminal (Winbox or SSH)
:local baseUrl "https://setup.hotelnet.io/scripts/"

# Download all 6 script files (no certificate files needed!)
/tool/fetch url=($baseUrl . "provision-check.rsc")  dst-path=provision-check.rsc  mode=https
/tool/fetch url=($baseUrl . "apply-config.rsc")     dst-path=apply-config.rsc     mode=https
/tool/fetch url=($baseUrl . "mqtt-subscribe.rsc")   dst-path=mqtt-subscribe.rsc   mode=https
/tool/fetch url=($baseUrl . "cmd-handler.rsc")      dst-path=cmd-handler.rsc      mode=https
/tool/fetch url=($baseUrl . "status-publish.rsc")   dst-path=status-publish.rsc   mode=https
/tool/fetch url=($baseUrl . "initial-setup.rsc")    dst-path=initial-setup.rsc    mode=https
With EMQX there are no sensitive certificate files to protect. All scripts are generic and safe to host on a public HTTPS URL — only the credentials in initial-setup.rsc need to be filled in per device.
4
Configure EMQX Credentials in initial-setup.rsc
Edit 5 variables in the file — no certificate import, no key files
~2 min

Open initial-setup.rsc in any text editor and fill in the 5 configuration variables at the top:

# ══════════════════════════════════════════════════════════
# !! CONFIGURE THESE 5 VALUES BEFORE RUNNING !!
# ══════════════════════════════════════════════════════════

# 1. Device Token from HotelNet Dashboard
:local deviceToken  "hnm_dev_tok_GRANDPALACE_001_xK9mPqR2"

# 2. Your EMQX broker address
:local mqttBroker   "mqtt.hotelnet.io"

# 3. MQTT port: 1883 = plain TCP  |  8883 = TLS
:local mqttPort     1883

# 4. EMQX username (from EMQX Dashboard → Access Control)
:local mqttUser     "device-AA-BB-CC-DD-EE-FF"

# 5. EMQX password
:local mqttPassword "StrongDevicePassword!2026"
That's it! These 5 values are stored as RouterOS environment variables when the script runs. The mqtt-subscribe.rsc reads them automatically on every boot — no hardcoded credentials in any running script.
EMQX — Simple Setup
  • No certificate files to generate or upload
  • Same script files for every device
  • One EMQX user per device — done in 30 seconds
  • Change password any time from EMQX dashboard
EMQX User Naming Convention
  • Format: device-{MAC-with-dashes}
  • Example: device-AA-BB-CC-DD-EE-FF
  • ACL rule: allow pub/sub cmd/AA:BB:CC:DD:EE:FF/# only
  • Or use shared credentials for simpler setup
5
Create RouterOS Scripts from .rsc Files
Load the 5 script files into RouterOS Script Manager, then run initial-setup.rsc
~5 min
Fastest method. The initial-setup.rsc file creates all scripts, sets up schedulers, and stores the device token automatically. Just paste your token and run.

1. initial-setup.rsc is pre-filled in Step 4. Just verify the 5 values are correct (device_token, mqtt_broker, mqtt_port, mqtt_user, mqtt_password), then run:

# Confirm values are filled in (not placeholder text)
:local deviceToken  "hnm_dev_tok_MUMBAI_GRAND_001_xK9mPqR2"  # ✓
:local mqttBroker   "mqtt.hotelnet.io"                        # ✓
:local mqttPort     1883                                      # ✓
:local mqttUser     "device-AA-BB-CC-DD-EE-FF"                # ✓
:local mqttPassword "StrongDevicePassword!2026"               # ✓

2. Open Winbox Terminal and run:

# Load initial-setup.rsc from the filesystem and execute it
/import initial-setup.rsc

# This script will:
# ✓ Store device_token, mqtt_broker, mqtt_port, mqtt_user, mqtt_password in RouterOS environment
# ✓ Create all 5 scripts from the .rsc files
# ✓ Set up 4 schedulers (boot MQTT, boot provision, daily 03:00, 60s status)
# ✓ Configure EMQX broker (username/password, no certs)
# ✓ Reboot the device to activate everything
After the reboot, the device will connect to MQTT and pull its config within 30 seconds. Watch the MQTT status in the platform dashboard go from offline → online.

Create each script individually in RouterOS Terminal:

# Step 5a: Store device token as environment variable
/system/script/environment/add name="device_token" \
  value="hnm_dev_tok_YOUR_PROPERTY_TOKEN_HERE"

# Step 5b: Create all scripts from uploaded .rsc files
/system/script/add name="provision-check" \
  source=[/file/get [find name="provision-check.rsc"] contents] \
  policy=read,write,policy,test,network,password

/system/script/add name="apply-config" \
  source=[/file/get [find name="apply-config.rsc"] contents] \
  policy=read,write,policy,test,network,password

/system/script/add name="mqtt-subscribe" \
  source=[/file/get [find name="mqtt-subscribe.rsc"] contents] \
  policy=read,write,policy,test,network,password

/system/script/add name="cmd-handler" \
  source=[/file/get [find name="cmd-handler.rsc"] contents] \
  policy=read,write,policy,test,network,password

/system/script/add name="status-publish" \
  source=[/file/get [find name="status-publish.rsc"] contents] \
  policy=read,write,policy,test,network,password

# Step 5c: Verify all 5 scripts were created
/system/script/print

# Step 5d: Create schedulers
/system/scheduler/add name="hotelnet-mqtt-connect" \
  start-time=startup interval=0 \
  on-event=":delay 15s; /system/script/run mqtt-subscribe" \
  comment="HotelNet: Connect MQTT 15s after boot"

/system/scheduler/add name="hotelnet-boot-provision" \
  start-time=startup interval=0 \
  on-event=":delay 30s; /system/script/run provision-check" \
  comment="HotelNet: Pull config 30s after boot"

/system/scheduler/add name="hotelnet-daily-provision" \
  start-time=03:00:00 interval=1d \
  on-event="/system/script/run provision-check" \
  comment="HotelNet: Nightly config refresh"

/system/scheduler/add name="hotelnet-status" \
  start-time=startup interval=1m \
  on-event="/system/script/run status-publish" \
  comment="HotelNet: 60s telemetry"

# Step 5e: Verify schedulers
/system/scheduler/print
WebFig is the browser-based interface at http://192.168.88.1. Useful when Winbox isn't available (e.g., macOS without Winbox).
  1. Open browser → go to http://192.168.88.1 (your MikroTik IP)
  2. Login as admin
  3. Navigate to System → Scripts
  4. Click Add New:
    • Name: provision-check
    • Policies: check all boxes
    • Source: paste the contents of provision-check.rsc
    • Click OK
  5. Repeat for each script: apply-config, mqtt-subscribe, cmd-handler, status-publish
  6. Navigate to System → Scheduler and create the 4 schedulers as shown in the Manual tab
  7. Navigate to System → Environment and add device_token variable with your token value
6
Configure MQTT Broker & Test Connection to EMQX
Manually set up the EMQX broker config once, then test the connection before rebooting
~3 min

Run these commands in Winbox Terminal or SSH. Replace values with your actual EMQX URL and credentials from device-info.txt:

# Get the MAC address of this device's WAN interface
:local mac [/interface get [find name="ether1"] mac-address]
:put ("Device MAC: " . $mac)

# Build client-id (ROS- prefix + MAC without colons)
:local clientId "ROS-"
:for i from=0 to=([:len $mac]-1) do={
  :local ch [:pick $mac $i ($i+1)]
  :if ($ch != ":") do={ :set clientId ($clientId . $ch) }
}

# Add the EMQX broker — username/password auth (no certificates)
/iot/mqtt/brokers/remove [find name="hotelnet"]

/iot/mqtt/brokers/add \
  name="hotelnet" \
  address="mqtt.hotelnet.io" \
  port=1883 \
  client-id=$clientId \
  username="device-AA-BB-CC-DD-EE-FF" \
  password="YourEmqxPassword" \
  ssl=no

# TLS (port 8883) — uncomment these instead if using TLS:
# port=8883  ssl=yes  (add: ssl-ca-cert="emqx-ca.pem_0" if you uploaded emqx-ca.pem)

# Connect to the broker
/iot/mqtt/brokers/connect [find name="hotelnet"]
:delay 5s

# Check connection status — look for connected=yes
/iot/mqtt/brokers/print

# Test publish
/iot/mqtt/publish broker="hotelnet" \
  topic=("status/" . $mac) \
  message=("{\"test\":true,\"mac\":\"" . $mac . "\",\"msg\":\"hotelnet_install_test\"}") \
  qos=0

# Check logs
/log/print where topics~"mqtt"
Success — What to Look For
  • /iot/mqtt/brokers/print shows connected=yes
  • Platform device record shows MQTT Status: Online
  • EMQX Dashboard → Cluster → Clients shows the device connected
  • Test publish does not show error in logs
Failure — Common Causes
  • not_authorized — Wrong username or password in EMQX
  • connection refused — Wrong broker URL or port blocked by firewall
  • timeout — No internet on ether1, or EMQX server unreachable
  • ACL denied — EMQX ACL rule doesn't match the topic pattern
Verifying in EMQX Dashboard

After connecting, open your EMQX Dashboard → Clients. You should see the device listed with client ID ROS-AABBCCDDEEFF, username, and connection status. Use the Topics tab to confirm subscriptions.

# Also verify from RouterOS:
/iot/mqtt/brokers/print detail
# Look for:  connected: yes   username: device-AA-BB-CC-DD-EE-FF
7
Run First Provisioning — Pull & Apply Config from Platform
Trigger the first config download and verify it applied correctly
~3 min

Before running, make sure the Config JSON is saved in the platform for this device's MAC. Then trigger the first provision:

# Method 1: Run provision-check script directly (recommended for first run)
/system/script/run provision-check

# Watch the log in real-time for success/error messages
/log/print follow-only

# You should see lines like:
# info: Provision: downloaded config from https://api.hotelnet.io/v1/devices/...
# info: Provision: applying new config hash sha256:abc123...
# info: apply-config: applying SYSTEM settings...
# info: apply-config: hostname set to GW-GRANDPALACE-01
# info: apply-config: VLAN 10 (GUEST) configured
# info: apply-config: HotSpot configured
# info: provision result published
# Method 2: Send MQTT provision command from platform dashboard
# Go to Dashboard → Device record → Actions → Send Command → provision
# Watch the provision result appear in the dashboard within 30 seconds
First provision may take 30–60 seconds as it downloads the full config JSON and applies all 8 sections (system, VLAN, DHCP, HotSpot, bandwidth, PoE, CAPsMAN, MQTT). Subsequent provisions are faster because unchanged sections are skipped via hash comparison.
Verify Applied Config
CheckRouterOS CommandExpected Result
Hostname set /system/identity/print name = GW-PROPERTYNAME-01
NTP sync /system/ntp/client/print status: synchronized
VLANs created /interface/vlan/print vlan10 GUEST, vlan20 STAFF, vlan99 MGMT
DHCP servers /ip/dhcp-server/print dhcp-guest, dhcp-staff running
HotSpot active /ip/hotspot/print hotspot1 running on vlan10
Config hash stored /system/script/environment/print device_token, cfg_hash variables present
Schedulers active /system/scheduler/print 4 hotelnet-* schedulers active
8
Final Reboot & Platform Confirmation
Reboot to simulate field conditions — verify full MQTT + provision cycle on clean boot
~3 min
# Reboot the device
/system/reboot

After reboot, the device will automatically execute this boot sequence:

0s
Device boots RouterOS
15s
MQTT connects to IoT Core
30s
Config pulled from API
45s
Config applied (if changed)
60s
First telemetry published
Confirm in HotelNet Dashboard
Device record shows MQTT Status: Online
Last Seen timestamp updated (within last 2 minutes)
Provision Status: success with applied config hash
CPU / RAM / Uptime telemetry values populated
Property's Network Devices tab starts populating with DHCP leases
Installation complete! The device is now fully onboarded to HotelNet MSP. Any future config changes made in the dashboard will auto-push to the device via the MQTT provision command or automatically apply on the next 03:00 AM nightly sync.
11

Technician On-Site Checklist

Print this and tick each item during hotel installation — don't leave without all green

Hardware & Physical
EMQX Credentials
Scripts & Schedulers
MQTT & Cloud Connectivity
Provisioning & Config
Final Verification
0 / 33 completed
12

Troubleshooting Guide

Quick diagnosis and fix for the most common installation and operation problems

MQTT MQTT broker shows "connected=no" or keeps reconnecting
Diagnostic steps:
# 1. Check stored EMQX credentials
/system/script/environment/print

# 2. Test TCP connectivity to EMQX
/tool/ping address="mqtt.hotelnet.io" count=3

# 3. Check logs for auth errors
/log/print where topics~"mqtt"

# 4. Try manual connect
/iot/mqtt/brokers/connect [find name="hotelnet"]
:delay 5s
/iot/mqtt/brokers/print detail
Common fixes:
not_authorized error — Wrong username or password in EMQX. Check EMQX Dashboard → Access Control → Authentication. Update stored password: /system/script/environment/set [find name="mqtt_password"] value="NewPass"
Firewall blocking port — Hotel firewall may block port 1883/8883 outbound. Test: /tool/ping mqtt.hotelnet.io. If blocked, configure EMQX to also listen on port 443 (MQTT over TLS), which is almost never blocked.
ACL denied — EMQX ACL rule doesn't match the topic. Check EMQX Dashboard → Access Control → Authorization. Add rule: allow this username to pub/sub cmd/{mac}/# and publish to status/{mac}
Wrong broker URL — Get exact EMQX address from device-info.txt or platform device record. Test: /tool/ping mqtt.hotelnet.io count=3
CONFIG provision-check script fails — "fetch failed" or "JSON parse error"
# Test the API call manually to see the actual error
:local mac [/interface get [find name="ether1"] mac-address]
:local token [/system/script/environment/get \
  [find name="device_token"] value-name=value]

# Test API fetch (will show HTTP error code if auth fails)
/tool/fetch url=("https://api.hotelnet.io/v1/devices/" . $mac . "/config") \
  http-method=get \
  http-header-field=("Authorization: Bearer " . $token) \
  dst-path=test-config.json \
  mode=https check-certificate=yes

# Check what was downloaded
/file/print detail where name="test-config.json"
:put [/file/get [find name="test-config.json"] contents]
401 Unauthorized — device_token is wrong or expired. Re-check it in the platform device record and update with: /system/script/environment/set [find name="device_token"] value="NEW_TOKEN"
404 Not Found — MAC address not registered in platform, or MAC format mismatch. Platform expects uppercase AA:BB:CC:DD:EE:FF format.
SSL error — Add check-certificate=no temporarily to diagnose. If it works without cert check, your CA chain is incomplete.
Config JSON not set — Go to platform → Device record → Config tab → fill in all required fields and save. The API returns 404 if no config exists yet.
SCRIPTS Scripts not created — "file not found" error during initial-setup.rsc import
# Check which files are actually present
/file/print

# If .rsc files are missing, re-upload them (see Step 3)
# Then verify they exist before running setup
/file/print where name~"rsc"
Files uploaded but not visible — RouterOS has a small RAM-based filesystem. Run /system/resource/print and check free-hdd-space. Need at least 100KB free. Delete old backup files if needed.
Script already exists error — Remove existing script first: /system/script/remove [find name="provision-check"], then re-run setup.
Policy permission error — Make sure your admin user has all policies. Run setup as the default admin user, not a restricted account.
CONFIG Config applies but device loses internet — routing broken after VLAN setup
# Diagnose routing after VLAN config
/ip/route/print
/ip/address/print

# Check if WAN interface ether1 still has its IP
/ip/dhcp-client/print

# Re-add WAN DHCP client if missing
/ip/dhcp-client/add interface=ether1 disabled=no

# Check firewall masquerade rule exists for WAN NAT
/ip/firewall/nat/print where action=masquerade
apply-config.rsc wiped bridge — If the script accidentally removed the bridge containing ether1, routing breaks. The script should only modify VLAN interfaces, not the WAN interface. Check apply-config.rsc script — look for any /interface/bridge/port/remove commands that might be too broad.
Factory reset recovery — If you're completely locked out: hold the reset button for 5 seconds to restore defaults, reconnect via ether2, and redo setup from Step 3.
HOTSPOT Guest WiFi visible but login portal not loading / captive portal redirect failing
# Check HotSpot status
/ip/hotspot/print detail

# Check HotSpot profile — verify login page URL
/ip/hotspot/profile/print

# Test if HotSpot DNS interception works
/ip/dns/print

# Check active HotSpot sessions
/ip/hotspot/active/print

# Check HotSpot cookies / walled garden
/ip/hotspot/walled-garden/print
Login URL wrong — The hotspot_login_page in config JSON must be the public URL of your portal. Check: /ip/hotspot/profile/print and verify login-by=http-chap and the HTML page URL
DNS not resolving — HotSpot requires DNS to work. Add portal domain to walled garden: /ip/hotspot/walled-garden/add dst-host="portal.hotelnet.io"
HTTPS redirect — Browsers use HSTS. If portal is HTTP, Chrome won't redirect. Use a proper HTTPS portal URL with valid SSL cert.
HotSpot on wrong interface — Verify hotspot is bound to the GUEST VLAN interface, not ether1 or bridge: /ip/hotspot/print → check interface=vlan10
CMDS Dashboard commands sent but device doesn't respond / ACK never received
# Check MQTT subscription is active
/iot/mqtt/print detail

# Run mqtt-subscribe script manually to re-subscribe
/system/script/run mqtt-subscribe

# Check if cmd-handler script exists and has no syntax errors
/system/script/print where name="cmd-handler"
/system/script/run cmd-handler  # will error if syntax issue

# Check IoT policy in AWS Console:
# IoT Core → Security → Policies → device policy
# Must allow: iot:Subscribe, iot:Receive on cmd/{mac}/#
# Must allow: iot:Publish on status/{mac}, cmd/{mac}/ack
MQTT subscription dropped — Long idle periods can cause broker to drop subscriptions. The hotelnet-mqtt-connect scheduler re-subscribes every boot but not during operation. Add a keepalive scheduler that pings the broker every 30 minutes.
Wrong MAC in topic — Topics use cmd/AA:BB:CC:DD:EE:FF/command format. Verify the device MAC in the platform matches exactly what RouterOS reports from ether1.
Quick Diagnostic Commands — Copy & Paste
System Health
/system/resource/print
/system/identity/print
/system/clock/print
/system/scheduler/print
/system/script/environment/print
/log/print where time>([/system/clock/get date] . " 00:00:00")
Network & MQTT
/iot/mqtt/brokers/print detail
/certificate/print
/ip/dhcp-client/print
/interface/vlan/print
/ip/hotspot/print
/ip/dhcp-server/lease/print count-only
13

Zero-Touch Installation

One URL command on the MikroTik — everything else is fully automatic

How it works: Pre-register the device MAC or serial in the platform. On-site technician runs a single 2-line command in RouterOS terminal. The device fetches install.rsc, calls the registration API, gets credentials, downloads all 5 scripts, connects to EMQX, and reboots — fully provisioned in under 60 seconds.
 Install Flow
1
Pre-Register
Admin adds MAC/serial in platform & assigns to property
2
Run One Command
Technician runs fetch + import on MikroTik — nothing else
3
Auto-Registers
Script POSTs MAC+serial to API — platform returns token & EMQX creds
4
Downloads & Installs
Downloads all 5 scripts, stores creds, creates schedulers, configures EMQX
5
Online in 45s
Reboots, connects EMQX, pulls config, applies — appears Online in dashboard
Step 1 — Admin: Pre-Register Device
Before the technician goes on-site, the admin registers the device in the platform by MAC address (printed on device sticker) or serial number (from /system/routerboard/print).
A Go to Super Admin → Gateways tab → click + Register New Device
B Enter MAC address (from label) or Serial number, select Property and Role
C Platform generates device_token + EMQX username/password — status becomes Pending
D Add the EMQX user in EMQX Dashboard → Access Control → Authentication with the generated username & password
Step 2 — Technician: Run One Command
Connect to the MikroTik via Winbox Terminal or SSH. Verify WAN internet is working, then run just these 2 lines:
/tool/fetch url="https://setup.hotelnet.io/install.rsc" \ dst-path=install.rsc mode=https check-certificate=no /import install.rsc
The device detects its own MAC and serial, calls the platform API, receives credentials, downloads scripts, and reboots. No password, no token, no cert files — the technician types nothing else.
~45 seconds after reboot — device appears Online in the Gateways tab with green MQTT status.
Registration API Contract — POST /api/v1/devices/register
Request (from install.rsc)
// POST https://api.hotelnet.io/v1/devices/register
// Content-Type: application/json
{
  "mac"         "AA:BB:CC:DD:EE:FF"
  "serial"      "HEX12345"
  "board"       "hEX S"
  "ros_version" "7.14.2"
}
Response (to install.rsc)
{
  "success"      true
  "device_id"    "dev-001"
  "device_role"  "gateway"
  "property_id"  "prop-001"
  "property_name""Grand Palace"
  "hostname"     "HN-GW-GRANDPALACE-01"
  "device_token" "hnm_XXXXXXXX_XXXXXXXX_XXXXXXXX"
  "mqtt_broker"  "mqtt.hotelnet.io"
  "mqtt_port"    1883
  "mqtt_user"    "device-AA-BB-CC-DD-EE-FF"
  "mqtt_password""StrongRand0mPassw0rd!"
  "mqtt_use_tls" false
}
If the device MAC or serial is not pre-registered in the platform, the API returns {"error":"device_not_found"} and the script stops with a clear error message. The technician will see: "Device not pre-registered. Go to Dashboard → Gateways → Register Device first."
Before vs After
Step Old (manual setup) New (zero-touch)
Technician knowsToken, EMQX URL, user, password, scripts URLOnly the URL: setup.hotelnet.io/install.rsc
Files to prepare6 .rsc files + optionally emqx-ca.pemNone — script downloads everything
Commands run~20 terminal commands (upload, env set, script create, scheduler, broker config)2 lines (fetch + import)
ConfigurationEdit initial-setup.rsc with token/password before uploadNone — API returns all config automatically
Time on-site15–20 minutes per device~2 minutes (connect WAN, run 2 commands, done)
Human error riskHigh (typos in credentials, wrong file versions)Near zero
Re-installRepeat all 20+ stepsReset to pending in dashboard, run 2 commands again
install.rsc Error Messages
registration API unreachable
No internet on ether1. Run /tool/ping 8.8.8.8 count=3 — if that fails, fix WAN connection first.
registration rejected
MAC/serial not in platform. Go to Gateways tab → Register New Device with the device MAC first.
already_active
Device is already online. If re-installing: open device in Gateways tab → click Reset for Re-install.
download failed: provision-check.rsc
CDN unreachable or setup.hotelnet.io DNS not resolving. Test: /tool/ping setup.hotelnet.io count=3
RouterOS too old
Requires v7.1+. Update: /system/package/update install
MQTT connect failed (non-fatal)
Script continues — EMQX connection retried after reboot via scheduler. Check EMQX user was added in EMQX Dashboard.
14

OpenWifi APs — Northbound API Integration

TIP OpenWifi (uCentral) cloud controller — REST + WebSocket management of all hotel APs

OpenWifi vs MikroTik MQTT: MikroTik gateways and CRS switches use EMQX MQTT (device-initiated southbound). OpenWifi APs use the uCentral Northbound REST API — the GuestXP platform calls the controller, which relays commands to APs over the permanent TLS southbound connection. No scripts needed on the AP.
 Architecture Comparison
 MikroTik Gateway / CRS Switch
Protocol: MQTT (EMQX)
Auth: username/password per device
Commands: publish to cmd/{mac}
Telemetry: device publishes to status/{mac}
On-device scripts: 5 .rsc files
Install: 2-line zero-touch command
 OpenWifi AP (uCentral)
Protocol: REST + WebSocket (Northbound)
Auth: Bearer JWT (controller issues)
Commands: POST /device/{serial}/command
Telemetry: GET /device/{serial}/status
On-device: none — ucentral-agent built into FW
Install: plug in → redirector → auto-claim
 GuestXP ↔ OpenWifi Controller Flow
1
Configure
Enter controller URL + credentials in AP Manager → Settings
2
Authenticate
POST /oauth2 → JWT Bearer token (auto-refreshed)
3
Sync APs
GET /inventory → merge all APs into AP Manager table
4
Live Status
WebSocket stream → real-time connect / client join events
Command
Reboot / Upgrade / Blink / Kick from AP Manager row actions
 Key Northbound Endpoints
Method Endpoint Purpose
POST/api/v1/oauth2Get Bearer token
GET/api/v1/inventoryAll APs + status
GET/api/v1/device/{serial}/statusLive radios + clients
POST/api/v1/device/{serial}/commandReboot/upgrade/blink/kick
PUT/api/v1/device/{serial}/configurePush full config blob
GET/api/v1/device/{serial}/statisticsTX/RX bytes, channel util
GET/api/v1/device/{serial}/logsAP log stream
WSS/api/v1/eventsReal-time event push
 Available AP Commands
reboot
{ "command": "reboot", "when": 0 }
upgrade
{ "command": "upgrade", "uri": "https://…/firmware.bin", "when": 0 }
leds (locate / blink)
{ "command": "leds", "pattern": "blink", "duration": 30 }
kick client
{ "command": "kick", "mac": "AA:BB:CC:DD:EE:FF" }
factory reset
{ "command": "factory_reset", "keep_redirector": true }
 WebSocket Real-Time Events
🟢
DEVICE_CONNECTED
AP came online — updates status to Online in AP Manager
🔴
DEVICE_DISCONNECTED
AP went offline — shows alert in Device Inventory
👤
CLIENT_JOIN / LEAVE
Guest device associated / disassociated — live client count
⬆️
FIRMWARE_UPGRADE_DONE
Upgrade completed — firmware version updated in AP record
Subscribe to events in GuestXP (js/openwifi-api.js):
OpenWifi.on('DEVICE_DISCONNECTED', msg => {
  const ap = AP_DATA.find(a => a.serial === msg.serialNumber);
  if (ap) { ap.status = 'offline'; renderAPManagerTable(); }
  showToast(`⚠️ AP ${msg.serialNumber} disconnected`, 'warning');
});

OpenWifi.on('CLIENT_JOIN', msg => {
  // Update client count live without polling
  const ap = AP_DATA.find(a => a.serial === msg.serialNumber);
  if (ap) { ap.clients++; renderAPManagerTable(); }
});
 AP Zero-Touch Onboarding
Step 1 — Pre-claim APs by serial number
POST /api/v1/inventory
{
  "serialNumber": "0000000012AB",
  "name": "AP-Room-301",
  "entity": "<venue-uuid>",
  "configuration": "<config-profile-uuid>"
}
Register each AP serial before shipping to site. The controller stores the config profile to push on first connect.
Step 2 — Plug in AP on site
1 AP boots → reads hardware MAC → contacts TIP redirector (DNS: ucentral-gw.wifi)
2 Redirector returns hotel controller URL → AP connects to owgw.hotel.com:16002
3 Controller finds pre-claimed serial → pushes config profile → AP applies SSIDs + radios
GuestXP next sync: AP appears Online in AP Manager with firmware version, client count, radio stats
 Controller Side (Self-Hosted)
 GuestXP Platform Side