A practical reference for network engineers building Python applications on Ericsson Cradlepoint routers.
The NetCloud OS (NCOS) SDK enables you to write Python applications that run directly on Ericsson Cradlepoint routers. These applications execute on the device itself, giving you programmatic access to modem diagnostics, WAN status, GPS, LAN clients, GPIO, and the full NCOS configuration and status tree — without additional hardware.
Use cases include automated site surveys, signal monitoring, data forwarding to cloud platforms (Splunk, Azure IoT, MQTT brokers), custom dashboards, OBD-II vehicle telemetry, geofencing, bandwidth management, and IoT device integration via serial or USB.
Applications are deployed either directly to a development device over SSH or at scale through Ericsson NetCloud Manager (NCM) using group-based assignment.
- TCP/UDP/SSL socket servers (ports above 1024) and clients
- Serial port access via PySerial (native and USB-serial)
- ICMP ping to external hosts
- Custom web interfaces (hotspot splash pages, dashboards, admin tools)
- Full read/write access to the NCOS API (status, config, and control trees)
- USB storage file access
- Container deployment via Docker Compose
- Compiled Python bytecode (
.pyc) and dynamically linked shared objects (.so) used as Python modules - Any operation requiring root or privileged permissions
- Python standard library modules not included in the NCOS environment (see Section 4)
Statically linked ARM64 (aarch64) ELF binaries are supported and can be bundled directly in your application package. See Section 5.5 for details.
Ericsson publishes and supports the SDK toolkit. However, custom applications built with the SDK are the sole responsibility of the developer. Ericsson does not develop, maintain, or guarantee continued compatibility of third-party SDK applications across NCOS firmware releases. Test thoroughly before production deployment.
- Python 3.8 or later on your development machine (Windows users: see WINDOWS_PYTHON_SETUP.md for detailed install steps)
- OpenSSL tools (for application signing)
- SSH access to a Cradlepoint router in Developer Mode (enabled via NetCloud Manager, not the router UI)
Clone the SDK and install Python dependencies:
git clone https://github.com/cradlepoint/sdk-samples.git
cd sdk-samplespython make.py setuppython3 make.py setupThis creates a .venv virtual environment and installs all Python dependencies from requirements.txt automatically.
Using Kiro? See docs/SETUP.md for a guided walkthrough — the Python environment is set up automatically.
To activate the virtual environment later:
# Windows
.venv\Scripts\activate.bat
# macOS / Linux
source .venv/bin/activateThe setup scripts handle Python libraries, but some system-level tools must be installed separately.
- Install OpenSSL (Light version) from slproweb.com. Choose Win64 or Win32 based on your machine.
# Install Homebrew (if not already installed)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
# Required for app signing and device deployment
brew install openssl
brew install hudochenkov/sshpass/sshpasssudo apt-get install libffi-dev libssl-dev sshpass python3-pip- A terminal with SSH/SCP support (PuTTY on Windows, native terminal on macOS/Linux)
You can run SDK applications directly on your development machine using standard Python. The cp.py module automatically detects whether it is running on a router (via the presence of /var/tmp/cs.sock) or on a computer. When running locally, all cp.get(), cp.put(), cp.post(), and cp.delete() calls are sent as HTTP REST requests to the router specified in sdk_settings.ini.
This means you can develop and test most application logic without deploying to the router on every change:
# Run your app locally — it talks to the router over REST
python3 my_app/my_app.pycp.get()/cp.put()/cp.post()/cp.delete()— all route through REST to the dev routercp.log()— prints to stdout (console) instead of syslog- All convenience functions that use
cp.get()internally (e.g.,cp.get_lat_long(),cp.get_connected_wans(),cp.get_signal_strength()) - Reading and writing appdata
| Function | Behavior When Local |
|---|---|
cp.alert() |
Logs the alert text to console but does not send to NCM |
cp.register() / cp.unregister() |
Event callbacks require the router's socket — no REST equivalent |
cp.decrypt() |
Logs a message and returns None — decryption requires the router |
Web servers (http.server) |
Binds to your local machine's port, not the router's — LAN clients cannot reach it |
| Serial port access | Accesses your computer's serial ports, not the router's |
| GPIO | Not available on your computer |
cppython-specific modules |
Your local Python may have different modules than the router |
- Use
sdk_settings.inito point at your dev router —cp.pyreads credentials from there automatically. - Test your core logic (API reads, data processing, decision-making) locally for fast iteration.
- Deploy to the router for final testing of alerts, event registration, web UIs, serial, and GPIO.
cp.log()output goes to your terminal when local, so you get immediate feedback.
Every SDK application follows a consistent directory layout:
my_app/
├── package.ini # Application metadata (UUID, version, vendor)
├── start.sh # Entry point — must use cppython
├── cp.py # SDK communication library (do not modify)
├── my_app.py # Your application logic
└── readme.md # Documentation and appdata field descriptions
Defines application metadata used by the router and NetCloud Manager:
[my_app]
uuid =
vendor = Ericsson/Cradlepoint
notes = SDK Application
version_major = 1
version_minor = 0
version_patch = 0
auto_start = true
restart = true
reboot = true
firmware_major = 7
firmware_minor = 25The uuid field is auto-generated when you create the app with make.py. The auto_start, restart, and reboot flags control whether the app starts automatically, restarts on crash, and survives device reboots.
The entry point script. It must use cppython (the router's Python interpreter), not python or python3:
#!/bin/bash
cppython my_app.pyThe SDK communication library. This file is auto-generated and should not be modified. It provides the cp module your application imports to interact with the router.
The router runs Python 3.8 via cppython, which is a constrained subset of a standard Python installation. Understanding these constraints is essential before writing code.
These modules are confirmed available on NCOS devices:
threading, select, ssl, http.server, socket, configparser, zipfile, io, hashlib, hmac, base64, struct, uuid, json, logging, os, sys, time, xml.etree.ElementTree
These common modules are not available in cppython:
pkg_resources, decimal, csv
If your application or a dependency requires these, copy the pure-Python shim files from existing sample apps (e.g., decimal.py, csv.py, _csv.py from the 5GSpeed or Mobile_Site_Survey directories).
Install libraries directly into your application directory:
pip3 install --target=my_app/ library_nameAfter installation, delete any egg-info or dist-info directories — they consume storage and are not needed at runtime.
Constraints:
- Only pure Python (
.py) files are supported. Remove any.pycor.sofiles. - Libraries must be compatible with Python 3.8.
- Keep dependencies minimal — app size limit is 60MB archive.
Since the runtime is Python 3.8, avoid newer syntax:
# Do NOT use union type syntax (Python 3.10+)
# value: str | None = None # WRONG
# Use Optional instead
from typing import Optional
value: Optional[str] = None # CORRECTThe cp module is your primary interface to the router. Import it at the top of your application:
import cpThere is no screen or console on the router. Use cp.log() for all output — never print():
cp.log('Starting my_app...')Log output is visible in NetCloud Manager and via make.py during development.
Use cp.get() to read from the status and config trees:
# Get system status
system = cp.get('status/system')
# Get modem signal information
wan_devices = cp.get('status/wan/devices')
# Get GPS coordinates
lat, lon = cp.get_lat_long()
cp.log(f'Location: {lat}, {lon}')Use cp.put() to modify configuration and cp.post() to create new entries:
# Set the device description
cp.put('config/system/asset_id', 'Site-42-Router')
# Trigger a control action
cp.put('control/system/reboot', '')Send alerts visible in NetCloud Manager:
cp.alert('WAN failover detected — switched to LTE backup')If your app needs internet access, wait for a WAN connection before proceeding:
cp.wait_for_wan_connection()
cp.log('WAN is up, proceeding...')Appdata provides per-app key-value storage configurable from NetCloud Manager. Use it for user-configurable settings:
# Read a setting
server_url = cp.get_appdata('server_url')
if not server_url:
server_url = 'https://default.example.com' # Code default, not written to appdata
cp.log('No server_url configured, using default')
# Write a value
cp.put_appdata('last_run', '2025-01-15T10:30:00')Never write default values to appdata — doing so overrides group-level configurations pushed from NCM.
Register callbacks that fire when specific API paths change:
def on_wan_change(path, value, args):
cp.log(f'WAN state changed: {value}')
cp.register('set', 'status/wan/connection_state', on_wan_change)Note: The callback receives exactly three arguments: (path, value, args) where args is a single tuple.
The cp module includes many high-level helpers. A selection of commonly used ones:
| Function | Description |
|---|---|
cp.get_lat_long() |
Returns (latitude, longitude) tuple |
cp.get_uptime() |
Router uptime in seconds |
cp.get_connected_wans() |
List of connected WAN interface names |
cp.get_sims() |
SIM card details for all slots |
cp.get_ipv4_lan_clients() |
Connected LAN clients by interface |
cp.get_signal_strength(uid) |
Modem signal metrics (RSRP, RSRQ, SINR) |
cp.get_temperature() |
Device temperature |
cp.get_power_usage() |
Power consumption data |
cp.get_wan_device_summary() |
Summary of all WAN devices and states |
cp.get_firmware_version() |
Current NCOS firmware version |
cp.get_serial_number() |
Device serial number |
cp.get_mac() |
Device MAC address |
cp.ping_host(host) |
Ping a remote host |
cp.reset_modem() |
Reset the cellular modem |
cp.get_ncm_api_keys() |
Retrieve NCM API credentials |
cp.extract_cert_and_key(name) |
Extract TLS certificate and private key |
For the complete API reference, see cp_methods_reference.md.
SDK applications can include statically linked ARM64 ELF binaries alongside (or instead of) Python code. The router's architecture is ARM64 (aarch64) with musl libc.
- The binary must be a statically linked, 64-bit ARM aarch64 ELF executable.
- Do not use dynamically linked binaries — the router does not have a standard Linux userland with shared libraries.
- Verify your binary before packaging:
file my_binary
# Expected: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, strippedAn application can consist entirely of a native binary with no Python code. In this case, start.sh launches the binary directly instead of cppython:
ttyd/
├── package.ini
├── start.sh # Launches the binary directly
├── cp.py # Still included (auto-generated)
├── ttyd # Statically linked ARM64 binary
└── readme.md
# start.sh — binary-only app
#!/bin/bash
./ttyd -p 8022 -W bashMore commonly, a Python application invokes a bundled binary using subprocess or os.system. The binary is placed in the application directory and called with a relative path:
my_app/
├── package.ini
├── start.sh # cppython my_app.py
├── cp.py
├── my_app.py # Python logic that invokes the binary
├── my_binary # Statically linked ARM64 binary
└── readme.md
import cp
import subprocess
cp.log('Running binary...')
try:
result = subprocess.run(['./my_binary', '--arg1', 'value'],
capture_output=True, text=True, timeout=120)
cp.log(f'Output: {result.stdout}')
if result.returncode != 0:
cp.log(f'Error: {result.stderr}')
except Exception as e:
cp.log(f'Binary execution failed: {e}')When sourcing binaries for NCOS applications, always download the aarch64, arm64, or linux-arm64 variant. Do not use x86_64 or amd64 builds. Many open-source projects publish static ARM64 builds on their GitHub releases pages.
Robust error handling is critical on embedded devices where failures must not crash the application.
Wrap all API calls and external operations in try/except blocks:
try:
data = cp.get('status/system')
if data:
cp.log(f"System ID: {data.get('system_id', 'unknown')}")
except Exception as e:
cp.log(f'Error reading system status: {e}')- Never use bare
except:— always catch a specific exception orException. - Never use
input()orKeyboardInterrupt— there is no keyboard. - Log errors with enough context to diagnose the issue remotely.
Every packaged application includes a MANIFEST.json with digital signatures. If any packaged file is modified at runtime, the router deletes the entire application. Always write to new files that were not part of the original package.
- Use relative paths only (e.g.,
data/results.json, not/tmp/results.json). - Create directories before writing with
os.makedirs('data', exist_ok=True). - Any directory name is fine — just ensure it does not overwrite packaged files.
Save application state to survive reboots:
import json, os
STATE_FILE = 'state.json'
def save_state(state):
with open(STATE_FILE, 'w') as f:
json.dump(state, f)
def load_state():
try:
with open(STATE_FILE, 'r') as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return {}SDK apps can serve web interfaces accessible from the router's LAN.
import cp
import json
import socket
from http.server import HTTPServer, SimpleHTTPRequestHandler
from threading import Thread
PORT = 8000
class MyHandler(SimpleHTTPRequestHandler):
def do_GET(self):
if self.path == '/api/status':
data = cp.get('status/system') or {}
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(data).encode())
else:
super().do_GET()
server = HTTPServer(('', PORT), MyHandler)
server.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
Thread(target=server.serve_forever, daemon=True).start()
cp.log(f'Web server started on port {PORT}')
# Main application loop
while True:
time.sleep(60)- Default to port 8000.
- Always set
SO_REUSEADDRbefore binding to avoid "Address in use" errors on redeployment. - Run the HTTP server in a daemon thread so the main thread can handle application logic.
- Serve all assets locally (CSS, JS, images). Do not reference external CDNs — the router may not always have internet access.
- All static files (HTML, CSS, JS) go in the application directory alongside your Python files.
python3 make.py create my_appThis generates all required files from the app_template directory. After creation, edit only my_app.py and readme.md — do not modify package.ini, start.sh, or cp.py.
Configure sdk_settings.ini in the repository root with your development router's connection details:
[sdk]
app_name=my_app
dev_client_ip=192.168.0.1
dev_client_username=admin
dev_client_password=your_password_here# Build the application package
python3 make.py build my_app
# Install to a locally connected router (must be in Developer Mode)
python3 make.py install my_app
# Check application status
python3 make.py statusThe build process creates a .tar.gz package containing all files in the application directory. This same package format is used for both local development and NCM deployment.
| Command | Description |
|---|---|
python3 make.py create my_app |
Scaffold a new application |
python3 make.py build my_app |
Build the .tar.gz package |
python3 make.py install my_app |
Deploy to development router |
python3 make.py start my_app |
Start the application |
python3 make.py stop my_app |
Stop the application |
python3 make.py status |
Check status of all installed apps |
python3 make.py uninstall my_app |
Remove from router |
python3 make.py purge |
Remove all installed applications from router |
python3 make.py clean my_app |
Remove local build artifacts |
Once your application is tested locally:
- Build the
.tar.gzpackage withmake.py build. - Upload the package to NetCloud Manager.
- Assign the application to a device group.
- NCM distributes and installs the application to all devices in the group.
Exclude files from the built package by creating a buildignore file in your app directory:
# Development files
test_data.json
requirements.txt
# Directories
tests/
docs/
The following are always excluded automatically: __pycache__/, buildignore, .DS_Store.
import cp
cp.log('Starting configurable app...')
# Read user-configurable values from NCM appdata
interval = cp.get_appdata('poll_interval')
interval = int(interval) if interval else 30 # Default 30 seconds
target = cp.get_appdata('target_host')
if not target:
target = '8.8.8.8'
cp.log('No target_host configured, using default')
cp.log(f'Polling {target} every {interval}s')import cp
import time
RSRP_THRESHOLD = -110 # dBm
cp.log('Starting signal monitor...')
cp.wait_for_wan_connection()
alerted = {} # Track which WANs have already triggered an alert
while True:
try:
wans = cp.get_connected_wans()
for wan_id in wans:
signal = cp.get_signal_strength(wan_id)
if signal:
rsrp = signal.get('rsrp')
if rsrp is not None and rsrp < RSRP_THRESHOLD:
if not alerted.get(wan_id):
cp.alert(f'Low signal on {wan_id}: RSRP={rsrp} dBm')
cp.log(f'Alert sent: {wan_id} RSRP={rsrp}')
alerted[wan_id] = True
else:
if alerted.get(wan_id):
cp.alert(f'Signal recovered on {wan_id}: RSRP={rsrp} dBm')
cp.log(f'Recovery alert sent: {wan_id} RSRP={rsrp}')
alerted[wan_id] = False
except Exception as e:
cp.log(f'Error in signal monitor: {e}')
time.sleep(300)Logs a GPS point every time the device moves a configurable distance, or at a slower interval while stationary. All thresholds are configurable via appdata.
import cp
import json
import math
import time
# Defaults — override via NCM appdata
MOVE_DISTANCE_M = 50 # Log when moved this many meters
STATIONARY_DIST_M = 10 # Movement below this is considered stationary
STATIONARY_INTERVAL = 300 # Seconds between logs while stationary
POLL_INTERVAL = 5 # Seconds between GPS checks
GPS_LOG = 'gps_log.json'
def haversine(lat1, lon1, lat2, lon2):
"""Distance in meters between two GPS coordinates."""
r = 6371000
p = math.pi / 180
dlat = (lat2 - lat1) * p
dlon = (lon2 - lon1) * p
a = (math.sin(dlat / 2) ** 2 +
math.cos(lat1 * p) * math.cos(lat2 * p) * math.sin(dlon / 2) ** 2)
return 2 * r * math.asin(math.sqrt(a))
def load_config():
"""Load thresholds from appdata, fall back to defaults."""
move = cp.get_appdata('move_distance_m')
stat_dist = cp.get_appdata('stationary_dist_m')
stat_int = cp.get_appdata('stationary_interval')
return (
int(move) if move else MOVE_DISTANCE_M,
int(stat_dist) if stat_dist else STATIONARY_DIST_M,
int(stat_int) if stat_int else STATIONARY_INTERVAL,
)
def write_entry(lat, lon):
entry = {'lat': lat, 'lon': lon, 'time': time.time()}
with open(GPS_LOG, 'a') as f:
f.write(json.dumps(entry) + '\n')
cp.log(f'GPS logged: {lat}, {lon}')
cp.log('Starting GPS logger...')
cp.wait_for_wan_connection()
last_lat, last_lon = None, None
last_log_time = 0
while True:
try:
move_dist, stat_dist, stat_interval = load_config()
lat, lon = cp.get_lat_long()
if lat is not None and lon is not None:
now = time.time()
if last_lat is None:
write_entry(lat, lon)
last_lat, last_lon, last_log_time = lat, lon, now
else:
dist = haversine(last_lat, last_lon, lat, lon)
if dist >= move_dist:
# Moved — log immediately
write_entry(lat, lon)
last_lat, last_lon, last_log_time = lat, lon, now
elif dist < stat_dist and (now - last_log_time) >= stat_interval:
# Stationary — log on timer
write_entry(lat, lon)
last_log_time = now
except Exception as e:
cp.log(f'GPS error: {e}')
time.sleep(POLL_INTERVAL)Cradlepoint routers support Docker containers via the NCOS container runtime, deployed through the REST API.
- Use
restart: unless-stopped(notrestart: always). - Named volumes require explicit
driver: local. - Memory limit directives (
mem_limit,deploy.resources.limits.memory) are not supported. Set `shm_size' at the service level to set shared memory size. - Prefer alpine-based images to minimize storage and RAM usage.
- Router architecture is ARM64 (aarch64) with musl libc — use
arm64image variants.
import cp
import json
compose_config = """version: "2.4"
services:
myservice:
image: myimage:latest
restart: unless-stopped
ports:
- "8080:8080"
volumes:
mydata:
driver: local
"""
project = {
'name': 'my_project',
'config': compose_config,
'enabled': True,
'update_interval': 0
}
cp.post('config/container/projects', json.dumps(project))
cp.log('Container project deployed')Application logs are accessible through:
make.py status— shows status of all installed apps- NetCloud Manager — device logs section
- SSH to the router — use the
logCLI command
The router uses a log command (not /var/log/messages). Common usage:
# Show all logs
log show
# Filter by app name or search string
log show -s my_app
# Follow logs in real time (like tail -f)
log show -f
# Follow with history (last 50 lines + new)
log show -f 50
# Case-insensitive search
log show -i -s "error"
# Highlight matches without filtering
log show -h -s my_app
# Filter by log level
log show WARNING ERROR
# Clear all logs
log clear# View current log level
log level
# Set log level
log level DEBUG
# View all service log levels
log service
# Change a specific service log level
log service level DEBUG# Write a message (defaults to INFO level)
log msg "Test message from CLI"
# Write at a specific level
log msg -l WARNING "Something needs attention"| Symptom | Cause | Solution |
|---|---|---|
| App deleted after install | Modified a packaged file at runtime | Write only to new files (e.g., tmp/) |
Address already in use |
Port still bound from previous deploy | Set SO_REUSEADDR, or reboot the router |
ModuleNotFoundError |
Missing dependency | pip3 install --target=my_app/ module_name |
.so or .pyc errors |
Non-pure-Python files in app | Remove all .so and .pyc files |
| App won't start | start.sh uses python3 instead of cppython |
Edit start.sh to use cppython |
| Stale log entries | Reading old log buffer | Check timestamps — only trust entries after your deploy |
- Edit your application code locally.
- Build and deploy:
python3 make.py build my_app && python3 make.py install my_app - Check status:
python3 make.py status
| Resource | URL |
|---|---|
| SDK GitHub Repository | github.com/cradlepoint/sdk-samples |
| Official SDK Developer Guide | docs.cradlepoint.com |
| cp Module API Reference | cp_methods_reference.md |
| Developer Community Portal | dev.cradlepoint.com |
| Customer Community Forums | customer.cradlepoint.com |
| NetCloud Manager SDK Tools | NCM Tools Tab |
| SDK Support Statement | SDK Support |
| Pre-built Sample Apps | Releases |