Installation & Deployment

Overview

This guide walks you through installing and running the IoT Emulator — a Spring Boot application (Java 25 bytecode, runs on JDK 27 in the image) that simulates IoT devices across multiple protocols. Docker is the recommended deployment method: it ships a self-contained image that runs the emulator in headless web mode and exposes the browser-based control panel and REST API on a single port.

The deployment consists of:

  • IoT Emulator — Spring Boot service started with --web-mode (no desktop/JavaFX GUI), serving the Web UI, the REST API and Swagger UI.

  • Docker container — pre-built image from the Harbor registry or loaded from an archive.

  • Configuration directory — /app/configuration inside the container, holding protocol resource files (*_resources.xml), GUI/property files, rule scripts and snapshots. It is baked into the image and can optionally be mounted as a volume for persistence and customization.

The emulator runs as a single container. It does not require its own database to start. A database connection is only needed for the optional Extract CPE from DB feature (see Extract CPE from DB), and network access to a target server (ACS, LWM2M server, MQTT broker, …​) is needed only when you actually start a simulation against it.

Prerequisites

System Requirements

  • Docker Engine 20.10+

  • Docker Compose 2.0+ (optional — only for the Compose deployment option)

  • Minimum 1 GB RAM (2+ GB recommended; more for large device counts)

  • 500 MB free disk space for the image and logs

JDK 25 is only required for local Maven builds and for running the desktop GUI without Docker. The Docker image runs on JDK 27 (eclipse-temurin:27-jre-alpine, bytecode still 25) from commit 4eece4d; images built before it run JDK 25. Docker deployment does not need a JDK on the host machine.

Supported Operating Systems

  • Linux (recommended)

  • macOS

  • Windows (Docker Desktop, or native Java for the desktop GUI)

Before starting, make sure:

  • Docker Engine 20.10+ is installed. If Docker is not installed, follow the Docker Installation Guide.

  • You have network access to any target server you intend to emulate against (ACS / LWM2M server / MQTT broker / STOMP broker).

  • If you plan to use Extract CPE from DB, you have network access to the ACS database (MySQL).

Third-Party Components

Every third-party product the emulator is built or run against. Values are read off this repository — the root pom.xml, the per-protocol module poms, and Dockerfile.

The table below is this emulator’s own view. For the platform-wide picture — which third-party product version each published release and hot-fix was built against, across every FT service — see FT Version Control.

Component Technology / Version Purpose

Container runtime

Docker Engine 20.10+, Docker Compose 2.0+

Recommended deployment method

Base image

eclipse-temurin:25-jre-alpine

Runtime image the emulator is built from (Dockerfile)

Java

25

Language level and runtime (pom.xml)

Application framework

Spring Boot 4.0.0-SNAPSHOT

Web UI and REST API (pom.xml)

springdoc-openapi

springdoc-openapi-starter-webmvc-ui 2.7.0

Serves Swagger UI and the OpenAPI spec (gui/pom.xml, application.yml)

Apache Commons Lang

3.17.0

Shared utility layer (pom.xml)

Apache Commons Collections

4.5.0-M2

LRU-bounded session cache for the USP MTP (usp-emulator/pom.xml), reaching the shipped gui image through gui’s dependency on `usp-emulator

Eclipse Californium

2.0.0-M16

CoAP and DTLS transport, used by the CoAP, LwM2M and USP emulators

Eclipse Leshan

1.0.0-M12

LwM2M client implementation (lwm2m-emulator/pom.xml)

Eclipse Paho MQTT v3

1.2.0

MQTT transport (mqtt-emulator/pom.xml, usp-emulator/pom.xml)

Eclipse Paho MQTT v5

1.2.5

USP MQTTv5 MTP (usp-emulator/pom.xml)

protobuf-java

4.29.2

USP record and message encoding (usp-emulator/pom.xml)

BouncyCastle

1.81 (bctls-jdk18on, bcprov-jdk18on)

TLS for the USP MQTTv5 MTP (mqtt-emulator/pom.xml)

Relational database

MySQL, used only by the optional Extract CPE from DB feature

Read through AcsDatabaseConnection (emulator-api), invoked from the LWM2M, MQTT and USP ACS services and from the GUI/REST controller; no database is needed to start the emulator or run a simulation

MySQL driver

com.mysql:mysql-connector-j (version managed by the spring-boot-starter-parent BOM)

JDBC access for Extract CPE from DB (lwm2m-emulator/pom.xml), reaching the shipped gui image through gui’s dependency on `lwm2m-emulator

Four of these are not release versions: spring-boot-starter-parent is a 4.0.0-SNAPSHOT, Californium is the 2.0.0-M16 milestone, Leshan is the 1.0.0-M12 milestone, and Apache Commons Collections is the 4.5.0-M2 milestone. They are listed as they are rather than rounded to 4.0.0 / 2.0.0 / 1.0.0 / 4.5.0, because a milestone and a release with the same number are not the same artifact and do not resolve to the same jar.

Beyond the optional MySQL connection above, the emulator needs no cache and no message broker of its own; the MQTT broker it talks to is part of the platform under test, not of this deployment.

Quick Start

For experienced users who already have Docker installed. The image ships with a complete default configuration directory, so no extra files are required to start.

# 1. Authenticate with the registry (first time only)
docker login hub.friendly-tech.com

# 2. Run the emulator (the image is pulled automatically if missing)
docker run -d \
  --name iot-emulator \
  -p 8556:8556 \
  hub.friendly-tech.com/emulator/iot:latest

# 3. Verify
curl -s http://localhost:8556/api/status

After startup the Web UI is available at http://localhost:8556. The root URL serves a landing page where you choose Load Test Mode (/load) or Single Emulator (/single) — see Operating Modes.

For repeatable deployments and upgrades that preserve your settings, snapshots and rule scripts, use the run-docker.sh bootstrap script from the repository instead of a raw docker run — see Deployment.

Getting the Docker Image

Option A: Pull from Harbor Registry (recommended)

# Authenticate with the registry
docker login hub.friendly-tech.com

# Pull the latest image (optional -- "docker run" / "docker compose up"
# will pull it automatically on first use)
docker pull hub.friendly-tech.com/emulator/iot:latest

To pin to a specific release tag instead of latest:

docker pull hub.friendly-tech.com/emulator/iot:<version>

<version> is an example placeholder. Always use the tag that corresponds to your deployment. To request Harbor access, contact the DevOps team for a user account or robot token.

Option B: Transfer Image to an Offline Server

Use this option when the target server has no internet access and cannot pull images from Harbor directly. The image is pulled on a machine that does have Harbor access, exported to a tar archive, transferred to the offline server, and loaded there.

  1. On a machine with Harbor access, log in and pull the image for the target server’s architecture:

    docker login hub.friendly-tech.com
    docker pull --platform linux/amd64 \
      hub.friendly-tech.com/emulator/iot:latest

    An explicit --platform matching the offline target server’s architecture is required if the image is multi-arch. Without --platform, docker pull selects the host architecture, which may not match the target (for example, an Apple Silicon arm64 Mac would produce an arm64 archive that fails on amd64 servers). Replace linux/amd64 with the platform of your offline target (linux/arm64, etc.).

  2. Save the pulled image to a tar archive and compress it:

    docker save hub.friendly-tech.com/emulator/iot:latest \
      -o iot-emulator.tar
    gzip iot-emulator.tar
  3. Transfer iot-emulator.tar.gz to the offline server (e.g., via scp or removable media).

  4. On the offline server, load the image:

    gzip -dc iot-emulator.tar.gz | docker load

Option C: Build from Source (developers only)

# Build the application JAR (Java 25 + Maven)
mvn clean package -DskipTests

# Build the Docker image (uses the Dockerfile in the repository root)
docker build -t iot-emulator:latest .

The Dockerfile copies gui/target/.jar and the gui/configuration directory into the image, so mvn package must run *before docker build.

After loading, pulling or building, verify the image is available:

docker images | grep iot

Configuration Directory

The configuration directory contains every file the emulator needs at runtime. It is baked into the image at /app/configuration/; when building from source the same files live in gui/configuration/.

configuration/
├── gui_connection_options.properties
├── lwm2m_gui.properties
├── lwm2m_controls_names.properties
├── mqtt_gui.properties
├── mqtt_controls_names.properties
├── usp_gui.properties
├── usp_controls_names.properties
├── fsh_hub_gui.properties
├── fsh_hub_controls_names.properties
├── profiles.properties
├── sensor_info.properties
├── metering_plugin_db_connection.properties
├── rules.js
├── snapshot.json
├── lwm2m_resources.xml
├── mqtt_resources.xml
├── usp_resources.xml
└── fsh_hub_resources.xml
File Description

gui_connection_options.properties

Main connection settings: server/client IP, port, security (PSK/X.509), protocol selection, GUI runtime flags (gui_rendering_type, client.logging, client.update_param, fail.response), and ACS DB credentials used by Extract CPE from DB (acs.ip, acs.port, acs.login, acs.password, acs.schema, acs.serial). Holds persisted user state.

*_gui.properties

Protocol-specific settings (LWM2M, MQTT, USP, FSH Hub)

*_controls_names.properties

GUI control labels for each protocol

profiles.properties

Active plugin/profile configuration

sensor_info.properties

Device sensor definitions

metering_plugin_db_connection.properties

Database connection for the metering plugin

rules.js

Rule engine script — body of onParamUpdated(param, device, ctx) (user state)

snapshot.json

Saved device list + parameter tree (user state)

*_resources.xml

Device data model definitions (LWM2M, MQTT, USP, FSH Hub). The active file is selected from the Device tree (XML) dropdown in the Web UI.

A bind mount on /app/configuration replaces the entire built-in directory. If you mount a partial directory (for example only the .xml files), the emulator will fail to start because the required .properties files are missing. To customize safely, first extract the full default directory from the image, edit it, then mount your complete copy — or use run-docker.sh, which does this overlay for you (see Deployment).

Deployment

Docker Compose with a per-service environment file is the supported form, and it is what the iot-emulator folder on FT_DISK ships: a single-service compose.yml in the platform layout and iot-emulator/.env. run-docker.sh remains the fastest path for a developer machine, and plain docker run the minimum.

FT_DISK Downloads

Download the following from FT_DISK — iot-emulator (FT_DISK → RD → prerelease → iot-emulator):

Component Target Files

Stack definition

/usr/local/ft-system/compose.yml

compose.yml — single-service (Option 1: Docker Compose (recommended)). Skip it on a host that runs the full FT System stack: its compose.yml already contains this service block under the testing profile, see Adding the emulator to an existing FT stack

IoT Emulator environment

iot-emulator/.env

Only one file: the .env in the folder root

The emulator has no shared infrastructure of its own, so there is no root .env layer in this deployment — the single iot-emulator/.env carries everything the container reads (Environment Variables).

Option 1: Docker Compose (recommended)

Lay out the deployment directory. The platform layout puts it under /usr/local/ft-system/, the same root every FT service uses; on a host of its own any directory works. The mounted configuration directory replaces the built-in one, so populate it from the image before the first start:

mkdir -p /usr/local/ft-system/iot-emulator/logs
cd /usr/local/ft-system

docker create --name tmp-emulator hub.friendly-tech.com/emulator/iot:latest
docker cp tmp-emulator:/app/configuration ./iot-emulator/configuration
docker rm tmp-emulator

The result:

/usr/local/ft-system/
├── compose.yml               # from FT_DISK -- iot-emulator (or the FT System <vendor> Stack)
└── iot-emulator/
    ├── .env                  # per-service environment, from FT_DISK
    ├── configuration/        # -> /app/configuration
    └── logs/                 # -> /app/logs

A new image does not change the files in configuration/. After each update, refresh the template files. Updating gives the steps.

compose.yml:

name: ft-iot-emulator

services:
  iot-emulator:
    image: hub.friendly-tech.com/emulator/iot:latest
    container_name: iot-emulator
    env_file:
      - ./iot-emulator/.env
    ports:
      # Host port is literal on purpose. The emulator does not read it, and
      # Compose interpolates ${...} only from the shell environment and from a
      # .env in this directory -- never from the env_file above.
      - "8556:8556"
    volumes:
      # The mounted directory REPLACES the built-in one, so it must contain
      # every required file. Populate it from the image before the first start.
      - ./iot-emulator/configuration:/app/configuration
      - ./iot-emulator/logs:/app/logs
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8556/api/status"]
      interval: 30s
      timeout: 3s
      start_period: 30s
      retries: 3
    restart: unless-stopped

iot-emulator/.env:

# ==========================================================
# IoT Emulator - per-service environment
# ==========================================================
# The only environment file this deployment uses. The emulator has no shared
# infrastructure of its own, so there is no root .env layer.
#
# The published host port is NOT here: it is a compose concern, not an
# application setting, and Compose does not interpolate env_file values anyway.
# Change it in compose.yml.

# HTTP port the emulator listens on inside the container
SERVER_PORT=8556

# Active Spring profile
SPRING_PROFILES_ACTIVE=prod

# Container timezone
TZ=UTC

# JVM options. Raise the heap for large device counts.
JAVA_OPTS=-Xms512m -Xmx1g -XX:+UseG1GC -XX:+UseStringDeduplication -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/app/logs/heapdump.hprof

# Auto-prepended by the launcher; opens sun.security.ssl for reflective TLS
# code paths. Normally leave as-is.
JDK_JAVA_OPTIONS=--add-opens=java.base/sun.security.ssl=ALL-UNNAMED

# USP MQTTv5 MTP TLS mode: 1 = mutual auth, 0 = server auth only
FT_TLS_AUTH=1

# Application log file inside the container
LOG_FILE=/app/logs/iot-emulator.log

# Headless mode only: override the target server at startup. Each is applied
# independently -- either can be set alone.
# SERVER_IP=
# SERVER_TARGET_PORT=

Start it:

docker compose up -d
docker compose logs -f iot-emulator

The published host port is not a .env variable. Compose resolves $\{…​} inside compose.yml from the shell environment and from a .env file in the same directory as compose.yml — never from a file listed under env_file:.

To publish on a different host port, change the left-hand side in compose.yml: "9556:8556". SERVER_PORT changes the port inside the container and has to match the right-hand side.

Adding the emulator to an existing FT stack

On a host that already runs the FT System stack, do not use the shipped compose.yml: it declares its own Compose project (name: ft-iot-emulator) with its own network, so the stack services are not reachable from it by name. The stack compose.yml from the FT System MySQL Stack / FT System Oracle Stack folder already carries the iot-emulator block, on the shared ft-network bridge:

  iot-emulator:
    image: hub.friendly-tech.com/emulator/iot:latest
    container_name: iot-emulator
    depends_on:
      ftacs:
        condition: service_healthy
    env_file:
      - ./iot-emulator/.env
    environment:
      TZ: ${TZ:-Europe/Kyiv}          # the host-wide zone from the root .env; overrides TZ in iot-emulator/.env
      JAVA_OPTS: -Xms512m -Xmx1g -XX:+UseG1GC
    ports:
      - "${IOT_EMUL_PORT:-8556}:8556"
    restart: unless-stopped
    profiles:
      - testing
    networks:
      - ft-network

Only iot-emulator/.env from FT_DISK is needed there. The block is behind the testing profile, so start it explicitly:

cd /usr/local/ft-system
docker compose --profile testing up -d iot-emulator

Note the differences from the single-service file: the published port comes from IOT_EMUL_PORT in the root .env; TZ is taken from the root .env through the environment: block (an env_file entry alone could not do that — iot-emulator/.env is loaded last and would win with its own TZ=UTC); JAVA_OPTS is pinned in environment: as well and therefore replaces the value in iot-emulator/.env, without the heap-dump flags; and neither the configuration directory nor the logs are mounted — settings, snapshots and rule scripts live inside the container and do not survive a recreation.

Option 2: run-docker.sh

The repository ships an idempotent bootstrap script, run-docker.sh. It is safe to re-run on every release: it pulls the latest image, overlays new/renamed template files (protocol options, resource files, control names) into ./configuration, and preserves user state — persisted settings, snapshots, rule scripts and any user-uploaded files such as TLS keystores — then recreates the container.

# From the directory where you want ./configuration to live
./run-docker.sh

What it does, in order:

  1. Pulls hub.friendly-tech.com/emulator/iot:latest.

  2. Extracts the image’s default configuration into a staging directory.

  3. Overlays template files (*_gui.properties, *_resources.xml, *_controls_names.properties, profiles.properties, sensor_info.properties, metering_plugin_db_connection.properties) onto ./configuration.

  4. Seeds user-state files (gui_connection_options.properties, snapshot.json, rules.js) only on first run — never overwriting them afterwards.

  5. Recreates the iot-emulator container.

The script deliberately differs from the Compose defaults above. It binds the UI to 127.0.0.1:8556 (loopback only) and sets FT_TLS_AUTH=0 (server-only TLS for the USP MQTT MTP). It also replaces JAVA_OPTS outright with -Xms1024m -Xmx16384m instead of adding to the image default, which drops -XX:+UseG1GC, -XX:+UseStringDeduplication and the HeapDumpOnOutOfMemoryError path. It never mounts /app/logs, and the unconditional docker rm -f it runs on every restart discards both the container’s logs and any heap dump along with it. Edit the variables at the top of the script (IMAGE, NAME, DATA) and the final docker run block to suit your environment — for example, change the port binding to -p 8556:8556 to expose the UI on all interfaces.

Option 3: docker run

For a throwaway container. The built-in configuration is used unless you mount your own:

docker run -d \
  --name iot-emulator \
  -p 8556:8556 \
  --restart unless-stopped \
  hub.friendly-tech.com/emulator/iot:latest

The same environment file works here too, which keeps the forms in sync:

docker run -d \
  --name iot-emulator \
  -p 8556:8556 \
  --env-file ./iot-emulator/.env \
  -v "$(pwd)/iot-emulator/configuration:/app/configuration" \
  --restart unless-stopped \
  hub.friendly-tech.com/emulator/iot:latest

Verify Installation

After starting the container, run the following checks:

# 1. Check container status
docker ps -f name=iot-emulator
# Expected: container "Up" with port 0.0.0.0:8556->8556/tcp

# 2. Status endpoint
curl -s http://localhost:8556/api/status
# Expected JSON, e.g.:
# {"protocol":"LWM2M","initialized":false,"started":false,"deviceCount":0}

# 3. Application logs
docker logs iot-emulator --tail 50
# Look for the Spring Boot "Started ..." line

# 4. Web UI (open in a browser)
# http://localhost:8556         -> landing page: pick a mode
# http://localhost:8556/load    -> Load Test mode
# http://localhost:8556/single  -> Single Emulator mode

# 5. Swagger UI (open in a browser)
# http://localhost:8556/swagger-ui.html

Environment Variables

These variables are baked into the image with sensible defaults. In the Compose deployment they live in iot-emulator/.env (see Option 1: Docker Compose (recommended)); with docker run they are passed as -e or through --env-file.

The published host port is deliberately absent from the table — it is a Compose concern and the application never reads it.

Variable Description Default Required

SERVER_PORT

HTTP port the emulator listens on inside the container

8556

No

SPRING_PROFILES_ACTIVE

Active Spring profile

prod

No

JAVA_OPTS

JVM options (heap sizing, GC, heap-dump on OOM). Increase the heap for large device counts. Carries --enable-final-field-mutation=ALL-UNNAMED, which the JDK 27 image needs; keep it when overriding.

--enable-final-field-mutation=ALL-UNNAMED -Xms512m -Xmx1g -XX:+UseG1GC …​

No

FTEMUL_VIRTUAL_THREADS

LwM2M scale mode for many devices per container (virtual threads). Same as -Dftemul.virtualThreads=true. See scale mode.

false

No

FTEMUL_X509_CA_PASSWORD_FILE

Path of a file holding the CA keystore password, for per-device X.509 minting. The only password source. See per-device credentials.

(unset)

No

FT_TLS_AUTH

USP MQTTv5 MTP TLS mode: 1 = mutual auth (client presents its certificate), 0 = server-only auth. See TLS Authentication Mode.

1

No

TZ

Container timezone

UTC

No

JDK_JAVA_OPTIONS

JVM options auto-prepended by the launcher (opens sun.security.ssl for reflective TLS code paths). Normally leave as-is.

--add-opens=java.base/sun.security.ssl=ALL-UNNAMED

No

SERVER_IP

Headless mode only. Overrides the target server IP at startup, without editing gui_connection_options.properties. Useful for Kubernetes/CI where the target is injected.

(unset)

No

SERVER_TARGET_PORT

Headless mode only. Overrides the target server port at startup. Applied independently of SERVER_IP — either can be set alone.

(unset)

No

SERVER_IP and SERVER_TARGET_PORT are read only when the emulator starts in headless web mode (--web-mode, which is how the Docker image runs). When set, they pre-populate the target connection so the container can connect to a server supplied at deploy time without a mounted configuration file.

SERVER_PORT changes the port inside the container. To reach it on a different host port, change only the left side of the Docker port mapping (e.g. -p 9556:8556); the container port stays 8556 unless you also change SERVER_PORT.

Application Properties

Beyond environment variables, runtime behaviour is driven by the files in the configuration directory. Most of these are managed for you through the Web UI and persisted automatically (see User Guide); you rarely need to edit them by hand.

TLS Authentication Mode

The emulator can switch between TLS mutual authentication and server-only authentication for the USP MQTTv5 MTP. It is controlled by the FT_TLS_AUTH environment variable (mapped to the JVM system property ft.tls.auth):

Value Behaviour

1 (default)

Mutual TLS authentication. The emulator presents its client certificate to the MQTT broker in addition to validating the server certificate.

0

Server-only authentication. The emulator validates the broker certificate but does not send a client certificate.

This setting currently affects only the USP MQTTv5 MTP. Other protocols ignore it and may use their own TLS configuration.

Docker:

docker run -d \
  --name iot-emulator \
  -p 8556:8556 \
  -e FT_TLS_AUTH=0 \
  hub.friendly-tech.com/emulator/iot:latest

Manual / run.sh / run.bat: export FT_TLS_AUTH before launching, or pass -Dft.tls.auth=0 directly to the JVM:

FT_TLS_AUTH=0 ./run.sh
# or
java -Dft.tls.auth=0 -jar gui/target/iot-emulator.jar --web-mode

USP Diagnostics Simulation

Two JVM system properties tune the synthesised USP diagnostics described in Supported Protocols — USP Diagnostics. The Ookla speed test itself is a per-device Protocol Options switch, not a system property: a device offers it only when that switch is on or when its device tree declares the command. If the switch is missing from the USP Protocol Options after an update, see An option is missing from Protocol Options after an update.

Property Default Effect

usp.diagnostics.ooklaDurationMs

5000

How long the Ookla speed test (X_T_SpeedTest()) runs before its OperationComplete is sent, in milliseconds. 0 sends it at once. Anything but a whole number of 0 or more falls back to the default.

usp.diagnostics.errorRate

(unset)

Share of diagnostic runs that fail, from 0 to 1 (0.25 fails one run in four). It applies to every synthesised diagnostic: the TR-181 commands report a DiagnosticsState of Error_…​, the Ookla speed test reports Status = Error. A failed run completes after 300 ms. Unset, 0 or a value that is not a number injects no failures.

Pass them through JAVA_OPTS, keeping the options that are already there: in iot-emulator/.env for the Compose deployment, or in the environment: block where a stack pins JAVA_OPTS (see Option 1: Docker Compose (recommended)). With docker run the variable replaces the image default, so repeat the heap options:

docker run -d \
  --name iot-emulator \
  -p 8556:8556 \
  -e JAVA_OPTS="-Xms512m -Xmx1g -XX:+UseG1GC -Dusp.diagnostics.ooklaDurationMs=30000 -Dusp.diagnostics.errorRate=0.2" \
  hub.friendly-tech.com/emulator/iot:latest

Manual:

java -Dusp.diagnostics.ooklaDurationMs=30000 -Dusp.diagnostics.errorRate=0.2 -jar gui/target/iot-emulator.jar --web-mode

Rules and Java Class Access

The emulator’s REST API is unauthenticated: anyone who can reach the HTTP port can change the configuration, start and stop devices, and save a JavaScript rule that then runs on every parameter update. Publish that port on a trusted network only. Rules cannot reach Java classes — Java.type(…​) and Packages…​. are refused, naming the class — so a rule is limited to the param, device and ctx objects documented in Rules. Set the JVM system property ft.rules.java to 1 to allow Java access again, from the next compile onwards; do this only where the port is already restricted, because it gives a rule everything the emulator process can read, including the mounted keystores and the ACS database password.

java -Dft.rules.java=1 -jar gui/target/iot-emulator.jar --web-mode

XML Resource Files

Resource files define the device data model. The active file is chosen from the Device tree (XML) dropdown in the Web UI; new ones can be uploaded there. Example structure:

<?xml version="1.0" encoding="UTF-8"?>
<resources>
    <resource>
        <path>Device.DeviceInfo.Manufacturer</path>
        <value>MyCompany</value>
        <type>string</type>
        <writable>false</writable>
    </resource>
    <!-- More resources... -->
</resources>

Manual Installation (without Docker)

Use this when you want to run the emulator directly on a host with Java 25 installed — either in headless web mode or as the desktop GUI.

Build from Source

  1. Clone the repository:

    git clone https://github.com/friendly-tech/iot-emulator.git
    cd iot-emulator
  2. Build with Maven:

    mvn clean package -DskipTests

Run in Web Mode (headless)

Web mode serves the same browser UI and REST API as the Docker container — ideal for servers without a display.

java -jar gui/target/iot-emulator.jar --web-mode

Run the Desktop GUI (JavaFX)

The assembled distribution includes launch scripts that start the native JavaFX desktop application:

# Linux / macOS
./run.sh

# Windows
run.bat

The desktop GUI requires JavaFX modules, which ship in the assembly under bin/. Set FT_TLS_AUTH before launching to control the USP MQTT TLS mode (see TLS Authentication Mode).

Container Management

Command Description

docker run -d --name iot-emulator -p 8556:8556 <image>

Start the emulator in the background

docker stop iot-emulator / docker start iot-emulator

Stop / start the container

docker restart iot-emulator

Restart the application

docker logs -f iot-emulator

Follow application logs

docker logs --tail 100 iot-emulator

View the last 100 log lines

docker exec -it iot-emulator sh

Open a shell inside the container

docker exec iot-emulator ls -la /app/configuration

List the active configuration files

docker stats iot-emulator --no-stream

View CPU / memory usage

docker inspect iot-emulator --format '{{json .Config.Env}}'

Show the container’s environment variables

Updating

A mounted configuration directory replaces the directory in the image. See Configuration Directory. A new image does not update these template files in the mounted directory:

  • *_gui.properties: protocol option definitions

  • *_resources.xml: device trees

  • *_controls_names.properties: control labels

  • profiles.properties, sensor_info.properties, metering_plugin_db_connection.properties: profile, sensor and metering plugin settings

Without a refresh, the new build uses the option definitions of the previous release. The /api/version response shows the new build. An option of the new release is still missing. For example, USP Protocol Options has no Ookla speed test (Tozed X_T_SpeedTest) switch.

run-docker.sh refreshes the template files itself. With Docker Compose or plain Docker, the update steps below include the refresh. A deployment without a mounted configuration directory always uses the files of the image. The FT stack block in Adding the emulator to an existing FT stack is such a deployment. The FT stack block needs no refresh.

Re-running the bootstrap script is the simplest upgrade path. It pulls the new image, propagates new template files, keeps your settings/snapshots/rules, and recreates the container.

# (Optional) back up your configuration first
tar -czf emulator-config-$(date +%Y%m%d).tar.gz configuration/

./run-docker.sh

Via Docker Compose

  1. Pull the new image:

    cd /usr/local/ft-system
    docker compose pull iot-emulator
  2. Refresh the template files in ./iot-emulator/configuration. See Refreshing the template files.

  3. Recreate the container:

    docker compose up -d iot-emulator
  4. Check the build of the running emulator:

    curl -s http://localhost:8556/api/version

    The response shows buildVersion, buildTime and commit of the new build.

Via plain Docker

  1. Pull the new image:

    docker pull hub.friendly-tech.com/emulator/iot:latest
  2. Refresh the template files in ./configuration. See Refreshing the template files.

  3. Recreate the container:

    docker rm -f iot-emulator
    docker run -d \
      --name iot-emulator \
      -p 8556:8556 \
      -v "$(pwd)/configuration:/app/configuration" \
      --restart unless-stopped \
      hub.friendly-tech.com/emulator/iot:latest
  4. Check the build of the running emulator:

    curl -s http://localhost:8556/api/version

    The response shows buildVersion, buildTime and commit of the new build.

If you are not mounting a configuration volume, recreating the container resets the configuration to the image defaults (snapshots and rule edits made through the UI are lost). Mount a volume, or use run-docker.sh, to keep user state across updates.

Every build of a release has the same version name, for example v.2.1.0. To tell two builds apart, compare buildVersion, buildTime and commit in the /api/version response.

Refreshing the template files

The commands below copy the template files of the new image into the mounted directory. Before you run the commands, set CONF to the mounted directory: ./iot-emulator/configuration for Docker Compose or ./configuration for plain Docker.

The copy overwrites your changes in a template file of the image, for example a changed usp_resources.xml. Keep a changed device tree under its own file name. Select the file in the Device tree (XML) list.

IMAGE=hub.friendly-tech.com/emulator/iot:latest
CONF=./iot-emulator/configuration

# (Optional) back up the directory first
tar -czf emulator-config-$(date +%Y%m%d).tar.gz "$CONF"

STAGE=$(mktemp -d)
docker create --name tmp-emulator "$IMAGE" >/dev/null
docker cp tmp-emulator:/app/configuration/. "$STAGE/"
docker rm tmp-emulator >/dev/null

for f in "$STAGE"/*_gui.properties \
         "$STAGE"/*_resources.xml \
         "$STAGE"/*_controls_names.properties \
         "$STAGE"/profiles.properties \
         "$STAGE"/sensor_info.properties \
         "$STAGE"/metering_plugin_db_connection.properties; do
  [ -f "$f" ] || continue
  cp "$f" "$CONF/"
done
rm -rf "$STAGE"

run-docker.sh makes the same copy. The copy keeps your user state:

  • gui_connection_options.properties: saved settings and selected option values

  • snapshot.json: saved device list and parameter tree

  • rules.js: rule script

The copy also keeps files from outside the image, for example TLS keystores or a device tree with its own file name.

Rollback

If the update refreshed the template files, restore the configuration backup before you start the previous image. Run tar -xzf on the archive from the same directory as tar -czf. The previous build then uses its own template files again. The restore also reverts the settings, snapshots and rules from after the backup.

docker rm -f iot-emulator

# Re-run with the previous image tag
docker run -d \
  --name iot-emulator \
  -p 8556:8556 \
  -v "$(pwd)/configuration:/app/configuration" \
  --restart unless-stopped \
  hub.friendly-tech.com/emulator/iot:<previous-version-tag>

Production / QA Checklist

# Item Notes

1

Restart policy

Use --restart unless-stopped (or restart: unless-stopped in Compose)

2

Configuration persistence

Mount a configuration volume, or deploy via run-docker.sh, so snapshots and rules survive restarts/updates. With a mounted volume, refresh its template files after each update. See Refreshing the template files

3

Heap sizing

Raise JAVA_OPTS (-Xmx) when simulating large device counts

4

Network exposure

Bind to loopback (-p 127.0.0.1:8556:8556) on shared hosts; expose on all interfaces only when needed

5

Health check

Configure a healthcheck against /api/status (already in the Compose example)

6

Timezone

Set TZ if logs must use a specific timezone (defaults to UTC)

7

Log management

Application logs go to /app/logs inside the container; mount it as a volume if you need them on the host

Troubleshooting

Start with quick diagnostics:

docker logs iot-emulator --tail 100
docker inspect iot-emulator --format '{{json .Config.Env}}'
docker exec iot-emulator ls -la /app/configuration

Common Issues

Container exits immediately / fails to start

Symptom: The container is not in the Up state, and logs mention a missing file under /app/configuration.

Check:

docker exec iot-emulator ls /app/configuration

Solution:

  • If you mounted a configuration volume, ensure it contains all required files (both .properties and .xml) — a bind mount replaces the entire built-in directory.

  • Re-extract the defaults (see Option 1: Docker Compose (recommended)) or deploy via run-docker.sh.

An option is missing from Protocol Options after an update

Symptom: /api/version shows the new build. An option of the new release is missing from Protocol Options. For example, USP Protocol Options has no Ookla speed test (Tozed X_T_SpeedTest) switch.

Check: compare usp_gui.properties in the container with the same file in the image. The commands look for the Ookla option. For another option, replace ookla with a part of its key.

# The file the running emulator reads (the mounted directory)
docker exec iot-emulator grep ookla /app/configuration/usp_gui.properties

# The same file in the image
docker run --rm --entrypoint grep hub.friendly-tech.com/emulator/iot:latest \
  ookla /app/configuration/usp_gui.properties

Solution:

  • If only the second command prints the option, refresh the template files. See Refreshing the template files.

  • If both commands print the option, select USP. Protocol Options shows only the options of the selected protocol.

In the first case, the mounted configuration directory has the template files of the previous release. After the refresh, reload the page. In Single Emulator mode, open Protocol Options again. A container restart is not necessary. The emulator reads the *_gui.properties file on every request.

Web UI not reachable on port 8556

Symptom: The browser cannot connect to http://localhost:8556.

Check:

docker ps -f name=iot-emulator      # is the port mapped?
curl -s http://localhost:8556/api/status

Solution:

  • Confirm the port mapping (-p 8556:8556).

  • If you deployed with run-docker.sh, the UI is bound to 127.0.0.1 only — access it from the host itself or change the binding to -p 8556:8556.

Port conflict (address already in use)

Symptom: The container fails to start because port 8556 is taken.

Check:

docker ps
lsof -i :8556     # Linux/macOS

Solution:

  • Stop the conflicting service, or publish a different host port: -p 9556:8556.

OutOfMemoryError with many devices

Symptom: A heap dump appears at /app/logs/heapdump.hprof; logs show OutOfMemoryError.

Solution:

  • Increase the JVM heap via JAVA_OPTS, e.g. -e JAVA_OPTS="-Xms1g -Xmx4g".

USP/MQTT TLS handshake fails

Symptom: The USP agent cannot connect to the MQTT broker over TLS.

Solution:

  • If the broker does not require client certificates, switch to server-only auth with -e FT_TLS_AUTH=0 (see TLS Authentication Mode).

  • Verify the keystore/truststore paths and the broker certificate chain.

Port Reference

Port Protocol Description

8556

HTTP

Web UI, REST API, and Swagger UI

Protocol traffic (CoAP/LWM2M 5683/5684, MQTT 1883/8883, USP MTP ports, …​) is outbound from the emulator to the target server, so it does not need to be published from the container. See Supported Protocols for protocol-specific defaults.