Why This Architecture Works
NAT traversal solved — devices initiate all connections outward
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.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.cmd/{mac}/# on your self-hosted EMQX broker. Cloud publishes commands (reboot, PoE toggle, provision, kick-guest) — device receives instantly and executes.status/{mac} every 60 seconds. All changes logged to audit trail.provision command. Device fetches + applies immediately. Use after config changes.Full System Architecture
Cloud components, MQTT broker, and on-premise MikroTik devices
/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.Auto-Provisioning Flow
Step-by-step: device boots → downloads config → applies → reports back
provision-check script. No cloud involvement needed./tool/fetch with Authorization: Bearer {token} header to GET full config JSON from the API. Token stored securely in RouterOS environment variables.provision/{mac}/result with success/error status and applied config hash.cmd/{mac}/#. Persistent session — commands queue while offline.cmd/{mac}/ack with result JSON. Dashboard shows real-time success/failure notification.Device Config JSON — REST API Response
Returned by GET /api/v1/devices/{mac}/config — full RouterOS configuration per device type
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
MQTT Topic Design
Complete topic structure for commands, status, provisioning, and events
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
Available MQTT Commands
All commands you can send from the dashboard to any RouterOS device
CSS3xx Switch — Gateway Proxy Pattern
How to control SwOS switches that have no MQTT support
/tool/fetch to call the SwOS HTTP API on the local network.
{
"target": "css3xx",
"css_ip": "192.168.100.20",
"cmd": "poe-toggle",
"port": 3,
"state": "off"
}
Security Model
EMQX username/password auth, per-device ACL topic isolation, command signing
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.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.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.What Needs to Be Built
Components required to complete this auto-provisioning system
Step-by-Step Installation Guide
Complete technician guide — from unboxing a MikroTik to fully provisioned + MQTT-connected
device_token, mqtt_user, and mqtt_password differ per device./iot/mqtt/). Supported models: RB4011iGS+, hEX S, hEX, RB5009, CCR2004, CRS317, CRS354. Check version: /system/package/printdevice-AA-BB-CC-DD-EE-FF), MQTT password, and MQTT broker URL. One set per device. No certificate files needed!hnm_dev_tok_XXXX_xxxxmt.lv/winbox. Alternatively use WebFig (browser) or SSH. Winbox is recommended for certificate upload.192.168.88.1. Default login: admin / no password./tool/ping 8.8.8.8 count=3/system/license/printprovision-check.rsc, apply-config.rsc, mqtt-subscribe.rsc, cmd-handler.rsc, status-publish.rsc, initial-setup.rsc/system/reset-configuration no-defaults=yesdevice-AA-BB-CC-DD-EE-FF, set a strong password
mqtt.hotelnet.io), mqtt_user, mqtt_password, and mqtt_port (1883 or 8883)
initial-setup.rsc — not hardcoded in any file.
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:
- Connect laptop to MikroTik ether2 with an Ethernet cable
- Open Winbox → click Neighbors tab → double-click your MikroTik → Login as
admin - In Winbox menu: Files → the File Manager window opens
- 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
- Wait for all uploads to complete (green progress bars)
- Verify files appeared: in Winbox Files, you should see all 6 .rsc files listed
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"
/) which maps to RouterOS filesystem root. Do NOT specify subdirectories — RouterOS scripts must be in the root.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
/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
initial-setup.rsc need to be filled in per device.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"
mqtt-subscribe.rsc reads them automatically on every boot — no hardcoded credentials in any running script.- 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
- 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
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
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
http://192.168.88.1. Useful when Winbox isn't available (e.g., macOS without Winbox).- Open browser → go to
http://192.168.88.1(your MikroTik IP) - Login as
admin - Navigate to System → Scripts
- Click Add New:
- Name:
provision-check - Policies: check all boxes
- Source: paste the contents of
provision-check.rsc - Click OK
- Name:
- Repeat for each script:
apply-config,mqtt-subscribe,cmd-handler,status-publish - Navigate to System → Scheduler and create the 4 schedulers as shown in the Manual tab
- Navigate to System → Environment and add
device_tokenvariable with your token value
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"
/iot/mqtt/brokers/printshowsconnected=yes- Platform device record shows MQTT Status: Online
- EMQX Dashboard → Cluster → Clients shows the device connected
- Test publish does not show error in logs
- 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
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
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
| Check | RouterOS Command | Expected 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 |
# Reboot the device
/system/reboot
After reboot, the device will automatically execute this boot sequence:
provision command or automatically apply on the next 03:00 AM nightly sync.Technician On-Site Checklist
Print this and tick each item during hotel installation — don't leave without all green
Troubleshooting Guide
Quick diagnosis and fix for the most common installation and operation problems
/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")
/iot/mqtt/brokers/print detail /certificate/print /ip/dhcp-client/print /interface/vlan/print /ip/hotspot/print /ip/dhcp-server/lease/print count-only
Zero-Touch Installation
One URL command on the MikroTik — everything else is fully automatic
install.rsc, calls the registration API, gets credentials, downloads all 5 scripts, connects to EMQX, and reboots — fully provisioned in under 60 seconds./system/routerboard/print).
POST /api/v1/devices/register// 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" }
{
"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
}
{"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."| Step | Old (manual setup) | New (zero-touch) |
|---|---|---|
| Technician knows | Token, EMQX URL, user, password, scripts URL | Only the URL: setup.hotelnet.io/install.rsc |
| Files to prepare | 6 .rsc files + optionally emqx-ca.pem | None — script downloads everything |
| Commands run | ~20 terminal commands (upload, env set, script create, scheduler, broker config) | 2 lines (fetch + import) |
| Configuration | Edit initial-setup.rsc with token/password before upload | None — API returns all config automatically |
| Time on-site | 15–20 minutes per device | ~2 minutes (connect WAN, run 2 commands, done) |
| Human error risk | High (typos in credentials, wrong file versions) | Near zero |
| Re-install | Repeat all 20+ steps | Reset to pending in dashboard, run 2 commands again |
/tool/ping 8.8.8.8 count=3 — if that fails, fix WAN connection first.setup.hotelnet.io DNS not resolving. Test: /tool/ping setup.hotelnet.io count=3/system/package/update installOpenWifi APs — Northbound API Integration
TIP OpenWifi (uCentral) cloud controller — REST + WebSocket management of all hotel APs
cmd/{mac}status/{mac}POST /device/{serial}/commandGET /device/{serial}/statusPOST /oauth2 → JWT Bearer token (auto-refreshed)GET /inventory → merge all APs into AP Manager table| Method | Endpoint | Purpose |
|---|---|---|
| POST | /api/v1/oauth2 | Get Bearer token |
| GET | /api/v1/inventory | All APs + status |
| GET | /api/v1/device/{serial}/status | Live radios + clients |
| POST | /api/v1/device/{serial}/command | Reboot/upgrade/blink/kick |
| PUT | /api/v1/device/{serial}/configure | Push full config blob |
| GET | /api/v1/device/{serial}/statistics | TX/RX bytes, channel util |
| GET | /api/v1/device/{serial}/logs | AP log stream |
| WSS | /api/v1/events | Real-time event push |
{ "command": "reboot", "when": 0 }
{ "command": "upgrade", "uri": "https://…/firmware.bin", "when": 0 }
{ "command": "leds", "pattern": "blink", "duration": 30 }
{ "command": "kick", "mac": "AA:BB:CC:DD:EE:FF" }
{ "command": "factory_reset", "keep_redirector": true }
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(); }
});
POST /api/v1/inventory
{
"serialNumber": "0000000012AB",
"name": "AP-Room-301",
"entity": "<venue-uuid>",
"configuration": "<config-profile-uuid>"
}