01 — Introduction
Introduction
What Mobius is
Mobius is server software that receives the data an IoT device sends, stores it, and makes it available for applications to read.
When every project builds that server for itself, the addressing rules, the data format and the way permissions are handled all end up different from product to product. oneM2M is the international standard that settles those questions, and Mobius implements the role that sits at the top of it: the IN-CSE, or Infrastructure Node Common Services Entity. Because Mobius follows the standard closely, devices and applications written against it also work against any other oneM2M platform without modification.
Mobius is written in Node.js and its source is open. It can store resources in either MySQL or SQLite, and which one it uses comes down to a single setting.
Devices push data up and applications pull it back down, while Mobius sits between them handling storage, identity and notifications.
Where it sits
Mobius is middleware placed between devices and applications. When a device uploads a reading, Mobius stores it, and an application then reads that value through the standard REST API. The same holds in the other direction: when an application writes a control value, any device that subscribed to the resource receives the change as a notification.
The tree becomes the address
oneM2M data is stored as a tree, much like folders in a file explorer, and the path down that tree becomes the URL. This means you never have to design an API for a resource you have just created: the moment the resource exists, so does the address you reach it at.
There are very few structural constraints. A container may hold another container, and the depth
is not limited. Every resource also carries the attributes the standard defines, among them the
creation time (ct), the name (rn), the expiration time
(et) and the access policy in effect (acpi).
Resource types
The table below lists the resource types Mobius can create. The ty number goes into
the request's Content-Type to tell the server which kind of resource you mean to create.
| ty | Name | What it does |
|---|---|---|
| 1 | accessControlPolicy | Decides who is allowed to do what |
| 2 | AE | A single device or application, and its entry point into the tree |
| 3 | container | Holds data, with optional retention limits |
| 4 | contentInstance | One reading, written once and never modified |
| 5 | CSEBase | The root of the tree, which is Mobius itself |
| 9 | group | Treats several resources as one |
| 10 | locationPolicy | Defines how location data is collected |
| 13 | mgmtObj | Device management objects such as firmware, battery and device info |
| 14 | node | A physical node |
| 16 | remoteCSE | Another CSE this one is registered with |
| 23 | subscription | Watches for change and sends notifications |
| 24 | semanticDescriptor | Carries semantic annotation |
| 27 | multimediaSession | A multimedia session |
| 28 | flexContainer | A container whose attributes you define freely |
| 91–98 | hd_* | Home-domain moduleClasses: lighting, colour, door lock, temperature, battery |
The SQLite backend supports six types: accessControlPolicy, AE,
container, contentInstance, CSEBase and
subscription. Creating a type outside that list is refused with 501
before anything is written, so the resource tree is never left half-built. If you need every
type, run on MySQL instead.
02 — Architecture
Architecture
The modules of Mobius are divided along the path a single request takes. Once you know that path, it becomes clear which file to open when you want to change a particular behaviour.
How a request is handled
An incoming HTTP request moves through the stages below. Each stage finishes only the part it owns and then hands the request on.
app.js and moves on to resource.js. The response always leaves through responder.js and nowhere else.
Two rules hold this flow together. First, sending the response and returning the database
connection happen exactly once per request. Second, the code that actually
writes a response lives in responder.js and nowhere else. Sending
twice, or failing to send at all, would leave a connection stranded, so both rules are enforced
by the structure of the code rather than by convention.
How the modules divide
These are the files you will find yourself opening most often.
| File | Responsibility |
|---|---|
mobius.js | The entry point. It only sequences the steps: read the configuration, check the port, write the boot record, start the server |
app.js | The HTTP server and the routing. It forks the workers and receives incoming requests |
mobius/resource.js | The core of resource handling, including per-type attribute checks and the create and update paths |
mobius/sql_action.js | Where every SQL statement is built, assembled by the query builder with values passed as bindings |
mobius/db/ | The database facade together with the backend adapters and the schema files |
mobius/security.js | Checks the access policy to decide whether a requester may touch a resource |
mobius/sgn.js · sgn_man.js | Finds the subscriptions, builds the notification body and sends it over HTTP, CoAP or MQTT |
mobius/responder.js · shape.js | The single exit a response leaves by, and the place the response body is assembled |
mobius/settle.js | The per-request settler that guarantees the response and the connection return happen only once |
mobius/conf_load.js · conf_schema.js | Where the configuration is read, and the single source of truth for which settings exist |
tools/ | The first-run wizard, the settings commands and the schema migration runner |
admin/ | The admin console, which runs as a process of its own |
Master and workers
Mobius forks one worker process per CPU core and spreads incoming requests across them. The master process takes no part in handling requests; it is responsible only for two jobs that run on a schedule.
- Retention sweep: deletes the oldest data once a container passes the count or size limit set on it. Because it counts the live rows again immediately before deleting, an inflated stored counter can never cause it to remove data that is still within the limit.
- Counter reconciliation: brings the count and size a container records back in line with the data actually stored.
If a worker exits unexpectedly, the master starts a replacement right away. When the exit was caused by a configuration problem, however — the port already in use, a missing configuration file, or a secret whose seal no longer matches — no replacement is started and the master exits with the same code. This is deliberate: a problem that a person has to fix should not be papered over by restarts.
The database layer
Building SQL and running it are kept apart. Once sql_action.js has finished a
statement, the facade in mobius/db/ passes it to the adapter for whichever backend
is currently selected. As a result, core code never needs to know whether it is running on MySQL
or on SQLite.
Because of that separation, switching backends takes a single setting, and adding a new one
amounts to dropping in a single mobius/db/<name>.js file. Every value that
goes into a query travels as a binding without exception, which is what makes SQL injection
through discovery parameters structurally impossible.
Directory layout
mobius.js # entry point — this is what you run app.js # HTTP server, workers, routing package.json # dependencies and npm commands conf.json # settings, written on the first start mobius/ # the core resource.js # resource handling sql_action.js # SQL construction security.js # access control sgn.js sgn_man.js # subscription notifications responder.js # response exit conf_load.js … # configuration loading ae.js cnt.js … # one file per resource type db/ index.js # backend facade mysql.js # MySQL adapter sqlite.js # SQLite adapter mobiusdb.sql # MySQL schema — import this at install time mobiusdb_sqlite.sql tools/ # setup wizard, settings commands admin/ # admin console (separate process) log/ # access log and boot record
03 — Installation
Installation
What you need
| Component | Required | Notes |
|---|---|---|
| Node.js | Yes | The runtime Mobius executes on. An LTS release is recommended |
| MySQL | Optional | The store for production. Needed if you want every resource type |
| SQLite | Automatic | Installed together with the modules, so there is nothing to prepare |
| MQTT broker | Optional | Only needed when notifications go to mqtt:// addresses, such as Mosquitto |
Start with SQLite. There is no database server to install, and the store is created for you on the first start. Node.js is the only thing you need in place.
Order of work
Installing Node.js
Download an LTS release from nodejs.org and install it. Once that is done, check in a terminal that both commands below print a version.
node -v npm -v
Preparing the database
Using SQLite
There is nothing to prepare. Running npm install in the next step brings in the
SQLite module, and the store file along with its tables is created the first time the server
starts.
Using MySQL
Install a MySQL server, create one database and import the schema file into it. That single import is the entire installation. The schema file already contains the tables, the indexes and the migration ledger, so nothing is left to apply afterwards.
# 1. create the database mysql -u root -p -e "CREATE DATABASE mobiusdb DEFAULT CHARACTER SET utf8mb3;" # 2. import the schema (run this from the Mobius source folder) mysql -u root -p mobiusdb < mobius/db/mobiusdb.sql
If you prefer a tool such as MySQL Workbench, create the mobiusdb schema and then
use Data Import → Import from Self-Contained File with
mobius/db/mobiusdb.sql. The result is exactly the same.
The default is mobiusdb. To use a different name, run
npm run conf -- set dbName <name> once the installation is finished.
MQTT broker (optional)
A broker is needed only if notifications will go to mqtt:// addresses. In that case,
install Mosquitto and leave its service running. If
your notifications go over HTTP only, you can skip this step entirely.
Source and modules
-
Get the source
terminalgit clone https://github.com/IoTKETI/Mobius.git cd Mobius
If Git is not installed, download the ZIP from the GitHub page and unpack it instead.
-
Install the modules
terminalnpm installWhen the command finishes and a
node_modulesfolder has appeared, you are done. Node.js has no build step, so the installation ends here.
04 — Running
Running
Starting for the first time
When there is no conf.json, Mobius runs the setup wizard by itself, so there is no
separate install command to remember.
node mobius.js
It asks seven questions in turn, saves your answers to conf.json, and then starts
the server straight away.
| Question | Default | Notes |
|---|---|---|
| Database | mysql | Choose either mysql or sqlite |
| DB password | — | Asked only when you chose MySQL, and hidden while you type |
| CSE name | Mobius | The name of the root. Every address begins with /<name> |
| CSE-ID | /Mobius2 | The identifier of this CSE |
| SP-ID | //keti.re.kr | The identifier of the service provider |
| Super-user origin | Sponde | A request sent with this origin passes every access check |
| HTTP port | 7579 | The port the server listens on |
Any request carrying that value in its header passes every access check. On a server reachable from outside, do not leave the default in place: change it to something hard to guess and keep the number of people who know it as small as possible.
If you kept the CSE name and the port at their defaults of Mobius and
7579, the top of your resource tree is now
http://localhost:7579/Mobius.
Starting it again
node mobius.js # use the settings in conf.json npm start # the same command node mobius.js sqlite # force SQLite for this run only node mobius.js mysql # force MySQL for this run only
Started for the first time under pm2 or systemd, where no terminal is attached, Mobius cannot
show the wizard and so exits without writing a configuration file. Run it once from a
terminal to create conf.json, then register it with the supervisor.
Checking that it works
Retrieve the top-level resource once. If a response like the one below comes back, the server is running correctly.
curl -i http://localhost:7579/Mobius \ -H "Accept: application/json" \ -H "X-M2M-RI: check1" \ -H "X-M2M-Origin: Sponde"
HTTP/1.1 200 OK
X-M2M-RSC: 2000
Content-Type: application/json
{"m2m:cb": {"rn": "Mobius", "ty": 5, "csi": "/Mobius2", …}}
Here X-M2M-RSC: 2000 is the success code defined by oneM2M. It is separate from the
HTTP status code, so it is worth reading both when you check a response.
Reading and changing settings
Settings are handled from the command line rather than through a web page. Since
conf.json holds sensitive values such as the database password and the super-user
origin, this is deliberate: only someone who can reach the server should be able to change them.
npm run conf # list every key and whether it is applied npm run conf -- csebaseport # look at one key in detail npm run conf -- set cseBase Vita # change a value npm run conf -- unset maxBodyBytes # restore the default npm run conf -- edit # walk through the main keys again npm run conf -- --all # include the advanced keys npm run status # is it running, is a restart pending
The configuration file is read once, when the server starts. A value you change therefore takes
effect only after a restart, and the npm run conf listing tells you which keys are
still waiting for one.
Keys that would cut existing devices off if set wrongly, such as the CSE name or the port, are marked as gated. Changing one of them prints a warning first and asks you to type the key name before the change is written.
Sealed secrets
The database password and the super-user origin are only ever changed through their own command.
If you open conf.json in an editor and edit either of them by hand, the next start
is refused. A companion file in the same folder, conf.seal.json, keeps a fingerprint
of the two values and catches the mismatch before the server comes up.
npm run setup -- --dbpass # re-enter the DB password npm run setup -- --superuser # re-enter the super-user origin
At either prompt you can simply press Enter, which keeps the current value and only recreates the seal. If you have upgraded from an older version and have no seal yet, doing this once is all that is needed.
Admin console
The admin console ships in the same repository as Mobius but runs as a separate process. It is an operator screen: it finds and clears expired resources and resources whose parent is gone, lets you review access policies, and shows subscription state and statistics.
# set a password first; without one the console will not start npm run conf -- set adminPassword 'your-password' # start the console node admin/server.js # or have Mobius start it as a child of the master process npm run conf -- set adminAutoStart on
Changing settings and controlling the process were deliberately left out of the console. Both of those are done with the commands described above.
When it will not start
Mobius reports why it failed to start through its exit code, so that it never ends up in the awkward state of being alive as a process while unable to serve any request.
| Code | Meaning | What to do |
|---|---|---|
| 12 | The port is already in use | Stop whatever is holding it, or change csebaseport |
| 13 | There is no conf.json | Run node mobius.js once from a terminal to create it |
| 14 | The seal on a secret does not match | Recreate it with npm run setup -- --superuser |
| 1 | The database could not be reached | Check that the database is up and that the password and name are right |
Per-request timings are not printed to the console; they are written as the last field of
log/access-*.log. The configuration a running server actually applied is recorded in
log/mobius-boot.jsonl, and npm run status reads that record and
presents it for you.
05 — Usage
Usage
The examples below work as written if you run them from top to bottom. They assume a server
running at localhost:7579 with the CSE named Mobius.
The shape of a request
Every request sent to Mobius needs at least two headers.
| Header | Purpose |
|---|---|
X-M2M-RI | The request identifier. Any value will do as long as it differs per request; it comes back unchanged in the response |
X-M2M-Origin | Who is asking. Access control decides on the basis of this value |
Content-Type | Needed only when creating a resource, as application/json; ty=<number>, which says what to create |
Accept | Set to application/json. Mobius speaks JSON and nothing else |
Registering an AE
Before anything else, a device or application registers itself as an AE, which
you can think of as claiming its own place inside the resource tree. Send exactly
S in the X-M2M-Origin header and Mobius will generate an identifier and
return it to you.
curl -X POST http://localhost:7579/Mobius \ -H "Accept: application/json" \ -H "X-M2M-RI: r-ae-1" \ -H "X-M2M-Origin: S" \ -H "Content-Type: application/json; ty=2" \ -d '{"m2m:ae": {"rn": "myAE", "api": "A.company.myapp", "rr": true}}'
{"m2m:ae": {
"rn": "myAE",
"ty": 2,
"aei": "S2026090812345601a3", <-- keep this value
"ri": "2-2026090812345601a3",
…
}}
The aei you get back serves as this AE's identity. From now on, put it in
X-M2M-Origin on every request this AE makes. If you would rather use an identifier
of your own, send that instead of S and it becomes the aei as it is.
rn: the resource name, which becomes one segment of the addressapi: the application identifier, a value the standard requiresrr: whether this AE is able to receive requests
Creating a container
A container is the vessel that holds your data, and it is created under the AE you just registered.
curl -X POST http://localhost:7579/Mobius/myAE \ -H "Accept: application/json" \ -H "X-M2M-RI: r-cnt-1" \ -H "X-M2M-Origin: S2026090812345601a3" \ -H "Content-Type: application/json; ty=3" \ -d '{"m2m:cnt": {"rn": "temperature", "mni": 1000}}'
mni sets how many entries this container will keep. Once that number is passed, the
master's retention sweep removes the oldest ones. To limit by size instead of by count, give
mbs in bytes. If you set neither, entries accumulate essentially without bound, so
it is worth deciding on one.
Storing and reading data
Actual readings are stored as a contentInstance. Since it is a record that is written once and never modified, the normal pattern is to add a new one each time a sensor reports a value.
curl -X POST http://localhost:7579/Mobius/myAE/temperature \ -H "Accept: application/json" \ -H "X-M2M-RI: r-cin-1" \ -H "X-M2M-Origin: S2026090812345601a3" \ -H "Content-Type: application/json; ty=4" \ -d '{"m2m:cin": {"con": "23.5"}}'
The content itself goes in con. That field is a string, so to store JSON you first
serialise it and put the string in.
Reading the latest value
curl http://localhost:7579/Mobius/myAE/temperature/la \ -H "Accept: application/json" \ -H "X-M2M-RI: r-la-1" \ -H "X-M2M-Origin: S2026090812345601a3"
la points at the most recently stored value and ol at the oldest one.
This is the address a dashboard uses most often when it needs to show a current reading.
Reading the container itself
curl http://localhost:7579/Mobius/myAE/temperature \ -H "Accept: application/json" \ -H "X-M2M-RI: r-cnt-2" \ -H "X-M2M-Origin: S2026090812345601a3"
In the response, cni is the number of entries currently stored and cbs
is how much space they take up in bytes.
Discovering resources
When you do not know what is there, you can walk the tree. Adding fu=1 to the query
returns a list of addresses for the resources that match.
curl "http://localhost:7579/Mobius?fu=1&ty=3" \ -H "Accept: application/json" \ -H "X-M2M-RI: r-dis-1" \ -H "X-M2M-Origin: Sponde"
{"m2m:uril": ["Mobius/myAE/temperature", "Mobius/myAE/humidity"]}
| Filter | Meaning | Example |
|---|---|---|
ty | Filters by resource type | ty=3 · ty=3&ty=4 |
rn | Matches the resource name exactly | rn=temperature |
lbl | Matches resources carrying that label | lbl=outdoor |
cra · crb | Created after, and created before | cra=20260101T000000 |
lvl | How many levels deep to walk | lvl=2 |
lim · ofst | How many to return, and how many to skip | lim=100&ofst=100 |
A response carrying X-M2M-CTS: 1 means there are more results still to come. Pass
the value in X-M2M-CTO as the ofst of your next request, and repeat
until you have collected them all.
Subscriptions and notifications
To be told whenever a new value arrives in a container, attach a
subscription to it and put the address you want notified in nu.
curl -X POST http://localhost:7579/Mobius/myAE/temperature \ -H "Accept: application/json" \ -H "X-M2M-RI: r-sub-1" \ -H "X-M2M-Origin: S2026090812345601a3" \ -H "Content-Type: application/json; ty=23" \ -d '{"m2m:sub": { "rn": "watch", "nu": ["http://192.168.0.10:9000/noti"], "nct": 2 }}'
From now on, every value added to that container triggers a POST to the address you gave. Since
nu is an array you can notify several places at once, and the form of each address
decides how the notification is delivered.
| Address | How it is delivered |
|---|---|
http://… | Sent as an HTTP POST. The receiver's response is read and logged as accepted, rejected or failed |
mqtt://… | Published over MQTT, which requires a broker to be configured |
coap://… | Sent as a CoAP request |
Mobius does not retry a notification, and it does not delete a subscription because delivery
failed. If the receiver happened to be down, those notifications are simply gone. For values you
cannot afford to miss, have the application poll la periodically as a backstop.
Access control
Out of the box, Mobius does not filter requests by who sent them. To control who may work with a particular resource, create an accessControlPolicy and attach it to that resource.
curl -X POST http://localhost:7579/Mobius \ -H "Accept: application/json" \ -H "X-M2M-RI: r-acp-1" \ -H "X-M2M-Origin: Sponde" \ -H "Content-Type: application/json; ty=1" \ -d '{"m2m:acp": { "rn": "readonly", "pv": {"acr": [{"acor": ["S2026090812345601a3"], "acop": 51}]}, "pvs": {"acr": [{"acor": ["Sponde"], "acop": 63}]} }}'
acop expresses the operations you allow as a sum of bit values.
| Value | Operation | Value | Operation |
|---|---|---|---|
| 1 | CREATE | 8 | NOTIFY |
| 2 | RETRIEVE | 16 | DELETE |
| 4 | UPDATE | 32 | DISCOVERY |
So 51 means create, retrieve, delete and discovery together (1+2+16+32), while
63 allows everything. pv governs the resources the policy is attached
to, and pvs governs who may modify the policy itself.
Attach the policy you created to the container from earlier.
curl -X PUT http://localhost:7579/Mobius/myAE/temperature \ -H "Accept: application/json" \ -H "X-M2M-RI: r-acp-2" \ -H "X-M2M-Origin: Sponde" \ -H "Content-Type: application/json" \ -d '{"m2m:cnt": {"acpi": ["/Mobius/readonly"]}}'
From this point on, only requesters the policy allows can work with that container. If you would like to see which requests a policy would block before you attach it, the access-policy screen in the admin console can simulate it for you.
Reading response codes
Every response carries both an HTTP status code and a oneM2M result code
(X-M2M-RSC). These are the ones you will meet most often.
| RSC | HTTP | Meaning |
|---|---|---|
| 2000 | 200 | The retrieve, update or delete succeeded |
| 2001 | 201 | The resource was created |
| 2002 | 200 | The resource was deleted |
| 2004 | 200 | The resource was updated |
| 4000 | 400 | The request was malformed; check the body and the headers |
| 4004 | 404 | No such resource exists |
| 4103 | 403 | An access policy refused the request |
| 4105 | 409 | A resource with that name already exists |
| 5000 | 500 | Something went wrong inside the server; check the log |
| 5001 | 501 | The current backend does not support that type |
When a request fails, the reason arrives as a single line in the m2m:dbg field of
the response body. That line is the first thing to read when you are working out what went wrong.
Where to go next
If you work with Postman, collecting these requests into a collection is worth the few minutes it takes. Put the server address and the CSE name into environment variables and you will not have to retype them again.
The source and the latest changes live in the GitHub repository. If you want to read about the standard itself, the specifications are published at oneM2M.