Repository navigation
Web API
The web_api module exposes a lightweight HTTP/JSON interface over the emulated
EtherNet/IP device. It lets you read and write tags, inspect datatype groupings,
and perform bulk writes, all over plain HTTP, without an EtherNet/IP client.
It is built on web.py and served via a background
wsgiref WSGI server thread, so it can run alongside the EtherNet/IP server
without blocking it.
from ethernetip_emulator.server.web_api import WebApiWebApi wraps a web.application with a fixed set of routes and binds them to
the same tag registry, actions, and device state used by the EtherNet/IP side
of the emulator. Every route ultimately calls back into
actions._lookup / actions._read_attr / actions._write_attr, so a write made
through the Web API fires the exact same on_change listeners as a write made
by the PLC.
The server itself runs in a daemon thread (start() / stop()), and WebApi
can be used as a context manager:
with WebApi("0.0.0.0", 8080) as api:
... # server is running
# server is stopped automatically on exitIn practice you will not construct WebApi directly. AttributeDevice owns a
single instance and manages its lifecycle for you, see
The Device Module for build_web / start_web /
shutdown.
| Path | Handler | Methods | Description |
|---|---|---|---|
/health |
health |
GET |
Liveness check. |
/tags |
tags |
GET |
List every tag with its current value, group, type, and read-only flag. |
/tag/(.+) |
tag |
GET, PUT, POST
|
Read or write a single tag by name. |
/datatypes/? |
datatypes |
GET |
List all datatype groups and the tags in each. |
/datatype/(.+) |
datatype |
GET |
List the tags belonging to a single datatype group. |
/bulk/? |
bulk |
POST, PUT
|
Write multiple tags in a single request, optionally atomically. |
All responses are JSON with Content-Type: application/json, produced by the
@endpoint decorator (see Error Handling below).
Returns a simple status payload. Useful for readiness/liveness probes.
{"status": "ok"}Returns every registered tag, serialized with its current value, datatype group, parser type name, and whether it is protected (read-only).
{
"tags": [
{
"name": "I_LotNumber.DATA",
"value": "LOT001",
"group": "STRING",
"type": "SSTRING",
"readonly": false
},
{
"name": "I_LotNumber.LEN",
"value": "LOT001",
"group": "STRING",
"type": "DINT",
"readonly": true
}
]
}Returns the serialized state of a single tag.
GET /tag/I_LotNumber.DATA
{
"name": "I_LotNumber.DATA",
"value": "LOT001",
"group": "STRING",
"type": "SSTRING",
"readonly": false
}Raises TagNotFoundError (404) if the tag does not exist.
Writes a new value to a tag. The request body must be JSON:
{"value": "LOT002"}- A scalar
valueis written toslice(0, 1). - A list
valueis written element-by-element, starting at index 0. - The tag's own
type_validatoris applied to each value before writing, so invalid values raiseTagTypeError(400) rather than corrupting the tag.
Returns the tag's new serialized state on success. Raises TagReadOnlyError
(403) if the tag is protected via AttributeDevice.protect.
Groups all tags by their datatype (e.g. bool, uintarray, string) and
lists the tags in each group, together with their parser type name.
{
"datatypes": [
{
"name": "BOOL",
"tags": {
"I_ProcessUnit": "BOOL",
"I_ClearLine": "BOOL"
}
},
{
"name": "UINTARRAY",
"tags": {
"O_Passed": "UINT"
}
}
]
}Groups are ordered by the datatypes known to actions.type, followed by any
additional groups found in the tag registry. Empty groups are omitted by
default.
Returns the tags belonging to a single named datatype group.
GET /datatype/bool
{
"name": "bool",
"tags": {
"I_ProcessUnit": "BOOL",
"I_ClearLine": "BOOL"
}
}Raises DatatypeNotFoundError (404) if the name matches neither a group
with tags nor a known datatype.
Writes several tags in one request. Request body:
{
"tags": {
"I_LotNumber.DATA": "LOT002",
"O_Step": [true, false, false]
},
"atomic": true
}| Field | Type | Default | Description |
|---|---|---|---|
tags |
object |
... | Mapping of tag name to the value(s) to write. Required. |
atomic |
bool |
false |
If true, every value is validated (and the tags checked for read-only) before any write happens. If any tag fails validation, no writes occur. |
Without atomic, tags are validated and written one at a time; a failure on
one tag does not prevent the others from being written. Response shape:
{
"tags": {
"I_LotNumber.DATA": {
"name": "I_LotNumber.DATA",
"value": "LOT002",
"group": "STRING",
"type": "SSTRING",
"readonly": false
}
},
"errors": {
"O_Step": {
"error": "tag_read_only",
"message": "tag 'O_Step' is read-only"
}
}
}The top-level errors key is only present if at least one tag failed.
Every route is wrapped with the endpoint decorator, which catches exceptions
raised inside the handler and turns them into a JSON error body with the
appropriate HTTP status:
| Exception | HTTP status |
error code |
|---|---|---|
TagNotFoundError |
404 Not Found |
tag_not_found |
TagReadOnlyError |
403 Forbidden |
tag_read_only |
TagTypeError |
400 Bad Request |
type_mismatch |
DatatypeNotFoundError |
404 Not Found |
datatype_not_found |
BadRequestError |
400 Bad Request |
bad_request |
TypeError (uncaught) |
403 Forbidden |
type_error |
| any other exception | 500 Internal Server Error |
internal_error |
All of these derive from ApiError, which supplies the status string and
code, and formats itself as {"error": code, "message": "..."}.
{"error": "tag_not_found", "message": "unknown tag 'DOES_NOT_EXIST'"}Unhandled, non-ApiError exceptions are logged with a full traceback
(log.exception) before being reported as a generic internal_error.
AttributeDevice owns the single WebApi instance used by the running
server, exposed as the class attribute AttributeDevice._web_api.
Constructs the WebApi instance (but does not start it):
AttributeDevice.build_web("0.0.0.0", 8080)Starts the web server thread if one was built with build_web. Binding
failures are handled gracefully rather than crashing the whole device:
-
PermissionError— logs a warning about binding to a privileged port (fix: run with elevated privileges or choose a port above 1023) and clears_web_api. -
OSError— logs a warning that the port is already in use and clears_web_api.
AttributeDevice.build_web("0.0.0.0", 8080)
AttributeDevice.start_web()Now also stops the Web API server as part of normal shutdown, before signalling the EtherNet/IP server to stop and resetting tag defaults:
AttributeDevice.shutdown()
# 1. WebApi.stop() if a web server is running
# 2. server_control["done"] = True, or SIGINT if no control dict is set
# 3. AttributeDevice.reset_defaults()# __main__.py
from ethernetip_emulator.server.device import AttributeDevice, apidict, device_controller
from ethernetip_emulator.server.tag_specs import tag_registry
import tags
import actions
if __name__ == "__main__":
server_control = apidict(timeout=1.0)
AttributeDevice.set_server_control(server_control)
AttributeDevice.build_web("0.0.0.0", 8080)
AttributeDevice.start_web()
with AttributeDevice._actions.bind(AttributeDevice):
device_controller(
argv=tag_registry.build_argv(base_args=["--print"]),
attribute_class=AttributeDevice,
server={"control": server_control},
)Because start_web fails soft, a bad host/port configuration will disable the
Web API for that run (with a warning) rather than preventing the EtherNet/IP
server from starting.
The module requires the third-party web.py package
(import web) in addition to the standard library wsgiref server used to
actually bind and serve HTTP requests.