A unified simulation platform that combines IP camera (RTSP) and radiation portal monitor (RPM) simulation into a single application with a web-based management interface. Designed to run as a systemd service inside a Proxmox LXC container.
| Simulator | Protocol | Default Port | Description |
|---|---|---|---|
| IP Camera | RTSP (TCP) | 9001+ | Loops a video file through MediaMTX, simulating a network camera |
| RPM Monitor | TCP ASCII | 10001+ | Streams Rapiscan VM250-format radiation data to connected clients |
The web UI runs on port 8000 and allows you to add, configure, and remove any number of camera streams and RPM monitors without editing any files or restarting the service.
- Debian/Ubuntu-based Linux (tested on Debian 12)
- Python 3.10 or later
ffmpeg(for camera simulation)- Internet access during installation (to download MediaMTX)
- Root or sudo access (for systemd service setup)
Note: Camera simulation requires
ffmpegand the MediaMTX binary, both of which are Linux x86_64 only. RPM simulation has no binary dependencies and works on any platform.
Resource usage is driven almost entirely by camera streams. FFmpeg is the heavy component β each transcoding stream consumes a full CPU core and significant RAM. RPM simulation is negligible regardless of how many monitors are configured (pure asyncio, ~1 MB per RPM).
An unprivileged container running Debian 12 works without any special configuration.
| Use case | vCPUs | RAM | Storage |
|---|---|---|---|
| RPM-only (no cameras) | 1 | 256 MB | 4 GB |
| 1β2 cameras, passthrough encoding | 2 | 512 MB | 10 GB + video files |
| 4β6 cameras, passthrough encoding | 2β4 | 1 GB | 20 GB + video files |
| 4β6 cameras, transcoding (resolution/FPS conversion) | 4β6 | 2 GB | 20 GB + video files |
Passthrough (-c copy) means FFmpeg reads the file and forwards it without re-encoding β very low CPU, roughly 50β100 MB RAM per stream. This is the default when you leave Resolution and Framerate blank in the Add Camera form.
Transcoding (any Resolution or Framerate override set) re-encodes with libx264 β expect ~1 vCPU and ~200β400 MB RAM per stream depending on resolution.
Storage: Budget for your video source files plus ~500 MB for logs over time. A typical 1080p security footage file is 500 MBβ2 GB depending on bitrate and length.
Network: Each RTSP stream consumes bandwidth proportional to its bitrate. Typical H.264 estimates:
| Resolution | Bitrate (per stream) |
|---|---|
| 1080p30 | 3β8 Mbps |
| 720p30 | 1β4 Mbps |
| 480p | 0.5β2 Mbps |
A 1 Gbps NIC is sufficient for any realistic number of streams. RPM data is negligible (<1 KB/s per monitor).
The same sizing applies. Run install.sh on any Debian/Ubuntu x86_64 system β it creates the lanesim systemd service identically to the LXC setup. No containerisation is required.
For a dedicated test bench with 4β8 camera streams and 10β20 RPM monitors, a machine with 4 cores and 4 GB RAM will be comfortable with headroom to spare.
For an OSCAR regression host that does not emulate cameras, use the RPM-only
installer. It avoids the camera installer's fixed x86-64 MediaMTX download,
creates 50 running RPMs on ports 10001 through 10050, enables recovery after
host or process restart, and installs the resource policy validated on the
50-lane Raspberry Pi test system.
sudo bash scripts/install-rpm-only-systemd.shThe default service policy is:
Restart=alwayswith a five-second delay and bounded restart rate;- 512 MiB memory pressure threshold and 768 MiB hard limit;
- 256-task and 4,096-file-descriptor limits;
- a non-login
lanesimservice account, restrictive umask, read-only application code, private temporary storage, and writable access only to simulator state, logs, and videos; - a post-start API check requiring all 50 RPMs to be running on the exact port range before installation succeeds.
The installer is configurable without editing it:
sudo env \
RPM_COUNT=50 \
RPM_BASE_PORT=10001 \
SITE_TZ=America/New_York \
MEMORY_HIGH=512M \
MEMORY_MAX=768M \
TASKS_MAX=256 \
bash scripts/install-rpm-only-systemd.shIt refuses to overwrite an existing /opt/lanesim. After reviewing the target,
set ALLOW_EXISTING_INSTALL=1; the existing tree is first copied to a timestamped
/opt/lanesim.pre-rpm-only.* backup. Existing RPM configuration is preserved
only when it already describes the requested running fixture. To deliberately
replace it, also set REPLACE_RPM_CONFIG=1. These acknowledgements do not replace
the need to preserve any test state that matters.
This installer changes only the application host. Firewall rules, DNS, OSCAR
lane configuration, and authorization to expose the management API remain the
operator's responsibility. The web UI listens on port 8000, and the generated
RPM listeners use the selected contiguous TCP range.
Copy the entire unified/ directory to the container. The installer uses its own location as the source, so the directory name does not matter.
scp -r unified/ root@<container-ip>:/tmp/lanesimVideo files can be uploaded at any time through the web UI after installation (see Video Library). If you want files available immediately on first boot, copy them into the videos/ subdirectory before running the installer and they will be copied automatically.
cp /path/to/your/*.mp4 /tmp/lanesim/videos/Three sample videos are included in the repository if you need them.
cd /tmp/lanesim
chmod +x install.sh
sudo bash install.shThe installer will:
- Install system packages:
ffmpeg,python3-venv,python3-pip,wget,tar - Create a
lanesimsystem user (no login shell) - Copy application files to
/opt/lanesim/ - Download and install the MediaMTX v1.9.0 binary
- Create a Python virtual environment and install dependencies
- Register and enable the
lanesimsystemd service
systemctl start lanesimOpen a browser and navigate to http://<container-ip>:8000.
| Task | Command |
|---|---|
| Start | systemctl start lanesim |
| Stop | systemctl stop lanesim |
| Restart | systemctl restart lanesim |
| Check status | systemctl status lanesim |
| View live logs | journalctl -u lanesim -f |
| Enable on boot | systemctl enable lanesim (done by installer) |
| Disable on boot | systemctl disable lanesim |
Navigate to http://<container-ip>:8000.
The dashboard has two tabs: Cameras and RPM Monitors. Both are managed independently from the same page. Configuration is persisted automatically β all cameras and RPMs are restored when the service restarts.
- Click + Add Camera.
- Fill in the form:
- Camera Name β a display label (e.g. "Lane 1 Entry")
- Video File β select from the uploaded files. If the list is empty, upload files first via π Videos (see Video Library).
- RTSP Port β auto-filled with the next unused port starting at 9001 (9001, 9002, β¦). You can edit it freely. Multiple cameras can share a port β they will use one MediaMTX instance on that port with different paths.
- Stream Path β the RTSP path segment (e.g.
cam1). The full URL will bertsp://<host>:<port>/<path>. - Resolution (optional) β transcode to 1080p, 720p, or 480p. Leave blank to pass through the source resolution.
- Framerate (optional) β transcode to a fixed FPS. Leave blank to pass through the source framerate.
- Click Start Stream.
The camera card will appear with its RTSP URL. Click the URL to copy it to the clipboard.
Connecting to a camera stream:
- VLC: Media β Open Network Stream β paste the RTSP URL
- ffplay:
ffplay rtsp://<host>:<port>/<path> - Any RTSP-capable NVR or video management software
Each camera card has a stop (square) button while active and a play button while stopped. Stopping a stream kills the FFmpeg process but keeps the configuration; clicking play restarts it with the same settings. The stopped/running state is persisted β a stopped camera remains stopped after a service restart.
While a camera is stopped, an edit (pencil) button appears on its card. Click it to open the configuration modal pre-filled with the current settings. Make your changes and click Save Changes. The new settings take effect the next time the stream is started.
You cannot edit a running camera. Stop it first.
Click the trash icon on the camera card. This stops the FFmpeg process and, if no other cameras are using the same RTSP port, also stops the MediaMTX instance for that port.
Click π Videos in the header to open the video library. This is where you manage the source footage available to camera streams.
Uploading a file:
- Drag and drop a video file onto the upload zone, or click the zone to open a file browser.
- A progress bar tracks large uploads.
- After upload the file is immediately available in the Add Camera dropdown.
Deleting a file:
- Each file in the library has a trash icon. Files currently in use by an active camera stream are labelled in use and cannot be deleted until that stream is removed.
Supported formats: .mp4, .mkv, .avi
- Click + Add RPM.
- Fill in the form:
- RPM Name β a display label (e.g. "Lane 2 Entry")
- TCP Port β the port this RPM will listen on (default: next available port starting at 10001). Each RPM requires its own unique port.
- Expand Advanced Settings to configure the simulation physics (optional β defaults match a typical VM250 deployment):
| Setting | Default | Description |
|---|---|---|
| Gamma Background | 120 | Nominal gamma detector counts per second |
| Neutron Background | 2 | Nominal neutron detector counts per second |
| Gamma Alarm Ο | 4.0 | Standard deviations above background to trigger a gamma alarm |
| Neutron Alarm Ο | 7.0 | Standard deviations above background to trigger a neutron alarm |
| Occupancy Frequency | 0.1 | Probability (0β1) of a vehicle occupancy starting each 5-second cycle |
| Occupancy Duration | 6,10 | Random vehicle transit time range in seconds (min,max) |
| Traffic Profile | off | Optional per-hour / per-weekday multipliers on Occupancy Frequency (see below) |
| Gamma Alarm Probability | 0.05 | Probability (0β1) that an occupancy contains a gamma alarm source |
| Neutron Alarm Probability | 0.01 | Probability (0β1) that an occupancy contains a neutron alarm source |
| Unique Alarm Probability | 0.0 | Probability (0β1) that an alarm uses spatially-unique source geometry |
- Click Start RPM.
The RPM card shows the current state (IDLE / OCCUPIED), connected clients, and running occupancy count. State and alarm badges refresh every 3 seconds.
By default a lane produces the same traffic at 3 a.m. on a Sunday as at 8 a.m. on a Tuesday. Tick Vary occupancy by time of day & day of week under Advanced Settings to shape it:
- Hourly bars β drag across the 24-bar chart to set a multiplier (0β3Γ) per hour, or type the values into the comma-separated field below it. The field is the easy way to copy a profile from one lane to another. Arrow keys nudge a focused bar.
- Day of week β a multiplier applied to the whole day, Monβ¦Sun.
- Presets β Flat, Commuter, Business hours and 24/7 port set both rows at once.
The effective arrival probability each 5-second cycle is:
p = clamp(Occupancy Frequency Γ hour_factor Γ day_factor, 0.0, 1.0)
Points worth knowing:
psaturates at 1.0. A base of 0.4 with a 3Γ peak clamps to 1.0, and pushing the multiplier higher does nothing. Bars in that state turn red and the readout says so.- The practical ceiling is ~277 vehicles/hour, not 720. A full arrival cycle is
5/pseconds of rolls plus one transit, because the simulator only rolls for a new vehicle while IDLE. The editor's veh/hr readout uses3600 / (5/p + mean_duration). - A factor of 0 means the lane is closed β background GB/NB still flows, but no occupancies start.
- Manual Ξ³/n triggers ignore the profile, so the trigger buttons still work during a zeroed hour.
- Hours follow the service's clock, set by
Environment=TZ=in the systemd unit (defaultAmerica/New_York; override at install time withSITE_TZ=...). The editor labels the readout with the live server timezone and outlines the current hour.GET /api/rpms/clockreports it. - The profile is stored per lane in
data/rpms.json. Lanes saved before this feature load with it disabled and flat β no migration needed.
Each RPM card has a stop button while active and a play button while stopped. Stopping an RPM shuts down the TCP server but retains the configuration. The stopped/running state persists across service restarts.
While an RPM is stopped, an edit button appears on its card. Click it to open the configuration modal pre-filled with all current settings. Save changes and start the monitor when ready.
You cannot edit a running RPM. Stop it first.
Running RPM cards have two alarm trigger buttons:
| Button | Label | Action |
|---|---|---|
| Gamma alarm | Ξ³ (orange) | Immediately starts an alarming occupancy with a gamma source |
| Neutron alarm | n (purple) | Immediately starts an alarming occupancy with a neutron source |
Clicking either button forces the RPM into OCCUPIED state and immediately latches the corresponding alarm, generating the appropriate GA/NA scan message on the next simulator update. This deterministic control behavior also applies to a one-second occupancy; it does not depend on the random count sample crossing the configured threshold. If the RPM is already in an occupancy, the alarm is added to the current transit and its scan timer is primed. Natural, non-manual alarm simulation continues to use the configured count thresholds. The occupancy runs for a randomised duration drawn from the configured Occupancy Duration range and then returns to IDLE automatically.
Click the trash icon on the RPM card. The TCP server is stopped and the port is released immediately.
RPMs stream raw ASCII text over a persistent TCP connection. Connect with any TCP client:
nc <container-ip> <port>Each line is terminated with \r\n.
| Message | When | Format | Description |
|---|---|---|---|
GB |
IDLE (every 5s) | GB,ddd,ddd,ddd,ddd |
Gamma background counts |
NB |
IDLE (every 5s) | NB,ddd,ddd,ddd,ddd |
Neutron background counts |
GS |
OCCUPIED (every 200ms) | GS,ddd,ddd,ddd,ddd |
Gamma scan, no alarm |
GA |
OCCUPIED (every 200ms) | GA,ddd,ddd,ddd,ddd |
Gamma scan, alarm latched |
NS |
OCCUPIED (every 1s) | NS,ddd,ddd,ddd,ddd |
Neutron scan, no alarm |
NA |
OCCUPIED (every 1s) | NA,ddd,ddd,ddd,ddd |
Neutron scan, alarm latched |
GX |
End of occupancy | GX,NNNNNN,NNNNNN,000000,000000 |
Occupancy summary (ID count, neutron cumulative) |
TT |
Tamper asserted and on client connect while active | TT,000000,000000,000000,000000 |
Tamper/power-fail state is active |
TC |
Tamper cleared and on client connect while clear | TC,111111,111111,111111,111111 |
Tamper/power-fail state is clear |
The four data fields represent the four detector channels: Driver Bottom, Driver Top, Passenger Bottom, Passenger Top.
Each new TCP client receives exactly one TT or TC baseline before the
normal detector stream. This lets consumers distinguish a known-clear RPM from
one whose tamper state has never been observed. The baseline always reflects
the simulator's current modeled state; reconnecting while tamper is active does
not falsely clear it.
Camera and RPM configurations are saved automatically to:
/opt/lanesim/data/cameras.json/opt/lanesim/data/rpms.json
These files are written atomically (write to .tmp, then rename) to prevent corruption on power loss. Each entry records both the device configuration and its last running state. When the service starts, devices that were running are restarted automatically; devices that were manually stopped remain stopped.
To reset all configuration, stop the service and delete both files:
systemctl stop lanesim
rm /opt/lanesim/data/cameras.json /opt/lanesim/data/rpms.json
systemctl start lanesimThe preferred method is the π Videos button in the web UI β drag and drop or browse to upload, no shell access needed (see Video Library).
Alternatively, copy files directly to the server. No restart is required.
cp /path/to/footage.mp4 /opt/lanesim/videos/
chown lanesim:lanesim /opt/lanesim/videos/footage.mp4| Log | Location | Contents |
|---|---|---|
| Service logs | journalctl -u lanesim -f |
Python application stdout/stderr |
| MediaMTX logs | /opt/lanesim/logs/mediamtx_<port>.log |
One file per RTSP port |
| FFmpeg logs | /opt/lanesim/logs/ffmpeg_<id>.log |
One file per camera stream |
| Port | Protocol | Purpose |
|---|---|---|
| 8000 | HTTP | Web management UI |
| 9001+ (default) | RTSP/TCP | Camera streams (auto-assigned from 9001, configurable per camera) |
| 10001+ (default) | TCP ASCII | RPM data streams (auto-assigned from 10001, configurable per RPM) |
All ports above 1024 β no special privileges required.
To update the application files after making changes:
cd /tmp/lanesim # updated source directory
sudo bash install.sh # re-copies files, re-installs Python deps
sudo systemctl restart lanesimExisting data/cameras.json and data/rpms.json are preserved; install.sh only copies .py files and the static/ and routers/ directories.
Service fails to start
journalctl -u lanesim -n 50Common causes: Python dependency missing (re-run install.sh), port 8000 already in use.
Camera shows "Stopped" immediately after adding
- Check that the video file exists in
/opt/lanesim/videos/ - Check the FFmpeg log:
/opt/lanesim/logs/ffmpeg_<id>.log - Confirm
ffmpegis installed:ffmpeg -version
MediaMTX fails to start
- Check the MediaMTX log:
/opt/lanesim/logs/mediamtx_<port>.log - Confirm the binary is executable:
ls -la /opt/lanesim/bin/mediamtx - Confirm no other process is using the port:
ss -tlnp | grep <port>
Cannot connect to RPM TCP port
- Confirm the RPM is shown as "Active" in the web UI
- Check the container firewall:
ufw statusoriptables -L - Confirm the port is listening:
ss -tlnp | grep <port>
Port conflict error when adding a device Both cameras and RPMs track used ports. If you see a 409 error, choose a different port. Camera RTSP ports can be shared across multiple camera streams β the conflict check only prevents assigning an RPM port that's already in use by another RPM or camera, and vice versa.