diff --git a/docs/components.md b/docs/components.md new file mode 100644 index 0000000..c869ddb --- /dev/null +++ b/docs/components.md @@ -0,0 +1,304 @@ +## Components + +Components in Moonraker are used to extend Moonraker's functionality, +similar to "extras" in Klipper. Moonraker divides components into +two categories, "core" components and "optional" components. A core +component gets its configuration from the `[server]` section and is +loaded when Moonraker starts. For example, the `file_manager` is a +core component. If a core component fails to load Moonraker will +exit with an error. + +Optional components must be configured in `moonraker.conf`. If they +have no specific configuration, a bare section, such as `[octoprint_compat]` +must be present in `moonraker.conf`. Unlike with core components, +Moonraker will still start if an optional component fails to load. +Its failed status will be available for clients to query and present +to the user. + +### Basic Example + +Components exist in the `components` directory. The below example +shows how an `example.py` component might look: +```python +# Example Component +# +# Copyright (C) 2021 Eric Callahan +# +# This file may be distributed under the terms of the GNU GPLv3 license. + +class Example: + def __init__(self, config): + self.server = config.get_server() + self.name = config.get_name() + + # Raises an error if "example_int_option" is not configured in + # the [example] section + self.example_int_opt = config.getint("example_int_option") + + # Returns a NoneType if "example_float_option is not configured + # in the config + self.example_float_opt = config.getfloat("example_float_option", None) + + self.server.register_endpoint("/server/example", ['GET'], + self._handle_example request) + + async def request_some_klippy_state(self): + klippy_apis = self.server.lookup_component('klippy_apis') + return await klippy_apis.query_objects({'print_stats': None}) + + async def _handle_example_request(self, web_request): + web_request.get_int("required_reqest_param") + web_request.get_float("optional_request_param", None) + state = await self.request_some_klippy_state() + return {"example_return_value": state} + +def load_component(config): + return Example(config) + +``` +If you have created a "Klippy extras" module then the above should look +look familiar. Moonraker attempts to use similar method for adding +extensions, making easier Klipper contributors to add new functionality +to Moonraker. Be aware that there is no "Reactor" object in Moonraker, +it uses `asyncio` for coroutines. Like Klippy, you should not write +code that blocks the main thread. + +### The ConfigWrapper Object + +As shown above, each component is passed a config object. This object +will be a `ConfigWrapper` type, which is an object that wraps a +configuration section to simply access to the native `ConfigParser`. +A `ConfigWrapper` should never be directly instantiated. + +#### *ConfigWrapper.get_server()* + +Returns the primary [server](#the-server-object) instance. + +#### *ConfigWrapper.get_name()* + +Returns the configuration section name associated with this `ConfigWrapper`. + +#### *ConfigWrapper.get(option_name, default=Sentinel)* + +Returns the value of the option`option_name` as a string. If +the option does not exist, returns `default`. If `default` is +not provided raises a `ConfigError`. + +#### *ConfigWrapper.getint(option_name, default=Sentinel)* + +Returns the value of the option`option_name` as an integer. If +the option does not exist, returns `default`. If `default` is +not provided raises a `ConfigError`. + +#### *ConfigWrapper.getfloat(option_name, default=Sentinel)* + +Returns the value of the option`option_name` as a float. If +the option does not exist, returns `default`. If `default` is +not provided raises a `ConfigError`. + +#### *ConfigWrapper.getboolean(option_name, default=Sentinel)* + +Returns the value of the option`option_name` as a boolean. If +the option does not exist, returns `default`. If `default` is +not provided raises a `ConfigError`. + +#### *ConfigWrapper.has_section(section_name)* + +Returns True if a section matching `section_name` is in the configuration, +otherwise False. + +Note that a ConfigWrapper object also implements `__contains__`, +which is an alias for `has_section`, ie: `section_name in config_instance` + +#### *ConfigWrapper.getsection(section_name)* + +Returns a Config object for the section matching `section_name`. If the +section does not exist in the configuration raises a `ConfigError`. + +Note that a ConfigWrapper object also implements `__getitem__`, +which is an alias for `get_section`, ie: `config_instance[section_name]` + +#### *ConfigWrapper.get_options()* + +Returns a dict mapping options to values for all options in the Config +object. + +#### *ConfigWrapper.get_prefix_sections(prefix)* + +Returns a list section names in the configuration that start with `prefix`. +These strings can be used to retreve ConfigWrappers via +[get_section()](#configwrappergetsectionsection_name). + +### The Server Object + +The server instance represents the central management object in Moonraker. +It can be used to register endpoints, register notifications, look up other +components, send events, and more. + +#### *Server.lookup_component(component_name, default=Sentinel)* + +Attempts to look up a loaded component, returning the result. If +the component has not been loaded, `default` will be returned. +If `default` is not provided a `ServerError` will be raised. + +#### *Server.load_component(config, component_name, default=Sentinel)* + +Attempts to load an uninitialized component and returns the result. It is +only valid to call this within a a component's `__init__()` method, and +should only be necessary if one optional component relies on another. Core components will always be loaded before optional components, thus an optional +component may always call +[lookup_component()](#serverlookup_componentcomponent_name-defaultsentinel) +when it needs a reference to core component. + +If the component fails to load `default` will be returned. If `default` +is not provided a `ServerError` will be raised. + +#### *Server.register_endpoint(uri, request_methods, callback, protocol=["http", "websocket"], wrap_result=True)* + +Registers the supplied `uri` with the server. + +The `request_methods` argument should be a list of strings containing any +combination of `GET`, `POST`, and `DELETE`. + +The `callback` is executed when a request matching the `uri` and a +`request_method` is received. The callback function will be passed a +`WebRequest` object with details about the request. This function +should be able of handling each registered `request_method`. The +provided callback must be a coroutine. + +The `protocol` is a list containing any combination of `http` and `websocket`. +If `websocket` is selected associated JSON-RPC methods will be generated based +on what is supplied by the `uri` and `request_methods` argument. A unique +JSON_RPC method is generated for each request method. For example: +```python +self.server.register_endpoint("/server/example", ["POST"], self._handle_request) +``` +would register a JSON-RPC method like: +``` +server.example +``` + +However, if multiple requests methods are supplied, the generated JSON-RPC +methods will differ: +```python +self.server.register_endpoint("/server/example", ["GET", "POST", "DELETE"], + self._handle_request) +``` +would register: +``` +server.get_example +server.post_example +server.delete_example +``` + +The `wrap_result` argument applies only to the `http` protocol. In Moonraker +all http requests return a result with a JSON body. By default, the value returned +by a `callback` is wrapped in a dict: +```python +{"result": return_value} +``` +It is only necessary to set this to false if you need to return a body that +does not match this result. For example, the `[octoprint_compat]` component +uses this functionality to return results in a format that match what +Octoprint itself would return. + +#### *Server.register_event_handler(event, callback)* + +Registers the provided `callback` method to be executed when the +provided `event` is sent. The callback may be a coroutine, however it +is not required. + +#### *Server.send_event(event, \*args)* + +Emits the event named `event`, calling all callbacks registered to the +event. All positional arguments in `*args` will be passed to each +callback. Event names should be in the form of +`"module_name:event_description"`. + +#### *Server.register_notification(event_name, notify_name=None)* + +Registers a websocket notification to be pushed when `event_name` +is emitted. By default JSON-RPC notifcation sent will be in the form of +`notify_{event_description}`. For example, when the server sends the +`server:klippy_connected` event, the JSON_RPC notification will be +`notify_klippy_connected`. + +If a `notify_name` is provided it will override the `{event_description}` +extracted from the `event_name`. For example, if the `notify_name="kconnect` +were specfied when registering the `server:klippy_connected` event, the +websocket would emit a `notify_kconnect` notification. + +#### *Server.get_host_info()* + +Returns a tuple of the current host name of the PC and the port Moonraker +is serving on. + +#### *Server.get_klippy_info()* + +Returns a dict containing the values from the most recent `info` request to +Klippy. If Klippy has never connected this will be an empty dict. + +### The WebRequest Object + +All callbacks registered with the +[register_endpoint()](#serverregister_endpointuri-request_methods-callback-protocolhttp-websocket-wrap_resulttrue) +method are passed a WebRequest object when they are executed. This object +contains information about the request including its endpoint name and arguments +parsed from the request. + +#### *WebRequest.get_endpoint()* + +Returns the URI registered with this request, ie: `/server/example`. + +#### *WebRequest.get_action()* + +Returns the request action, which is synonomous with its HTTP request +method. Will be either `GET`, `POST`, or `DELETE`. This is useful +if your endpoint was registered with multiple request methods and +needs to handle each differently. + +#### *WebRequest.get_connection()* + +Returns the associated Websocket connection ID. This will be `None` +for HTTP requests when no associated websocket is connected to +the client. + +#### *WebRequest.get_args()* + +Returns a reference to the entire argument dictionary. Useful if +one request handler needs to preprocess the arguments before +passing the WebRequest on to another request handler. + +#### *WebRequest.get(key, default=Sentinel)* + +Returns the request argument at the provided `key`. If the key is not +present `default` will be returned. If `default` is not provided a +`SeverError` will be raised. + +#### *WebRequest.get_str(key, default=Sentinel)* + +Retrieves the request argument at the provided `key` and converts it +to a string, returning the result. If the key is not present the `default` +value will be returned. If `default` is not provided or if the attempt at +type conversion fails a `SeverError` will be raised. + +#### *WebRequest.get_int(key, default=Sentinel)* + +Retrieves the request argument at the provided `key` and converts it +to an integer, returning the result. If the key is not present the `default` +value will be returned. If `default` is not provided or if the attempt at +type conversion fails a `SeverError` will be raised. + +#### *WebRequest.get_float(key, default=Sentinel)* + +Retrieves the request argument at the provided `key` and converts it +to a float, returning the result. If the key is not present the `default` +value will be returned. If `default` is not provided or if the attempt at +type conversion fails a `SeverError` will be raised. + +#### *WebRequest.get_boolean(key, default=Sentinel)* + +Retrieves the request argument at the provided `key` and converts it +to a boolean, returning the result. If the key is not present the `default` +value will be returned. If `default` is not provided or if the attempt at +type conversion fails a `SeverError` will be raised. diff --git a/docs/index.md b/docs/index.md index 13b7523..0f93305 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,7 +7,9 @@ to install and configure Moonraker. Client developers may refer to the [Client API](web_api.md) documentation. -Internal API documentation for back-end contributors is forthcoming. -In the meantime developers should refer to the +Backend developers should refer to the [contibuting](contributing.md) section for basic contribution -guidelines prior to creating a pull request. +guidelines prior to creating a pull request. The +[components](components.md) document provides a brief overview +of how to create a component and interact with Moonraker's +primary internal APIs. diff --git a/docs/installation.md b/docs/installation.md index 0d29a88..fb75c19 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -64,9 +64,10 @@ KLIPPY_EXEC=/home/pi/klippy-env/bin/python KLIPPY_ARGS="/home/pi/klipper/klippy/klippy.py /home/pi/printer.cfg -l /tmp/klippy.log -a /tmp/klippy_uds" ``` -**Note: Your installation of Klipper may use systemd instead of -the default LSB script. In this case, you need to modify the -klipper.service file.** +!!! note + Your installation of Klipper may use systemd instead of + the default LSB script. In this case, you need to modify the + klipper.service file. You may also want to take this opportunity to change the location of printer.cfg to match Moonraker's `config_path` option (see the diff --git a/docs/plugins.md b/docs/plugins.md deleted file mode 100644 index 5f9055e..0000000 --- a/docs/plugins.md +++ /dev/null @@ -1,3 +0,0 @@ -## Plugins - -Documentation Forthcoming diff --git a/docs/printer_objects.md b/docs/printer_objects.md index 82dd27d..044e11a 100644 --- a/docs/printer_objects.md +++ b/docs/printer_objects.md @@ -61,8 +61,9 @@ The `gcode_move` object reports the current gcode state: the most recent "G1" or "G0" processed assuming the machine is using absolute coordinates. -Note: The printer's actual movement will lag behind the reported positional -coordinates due to lookahead. +!!! Note + The printer's actual movement will lag behind the reported positional + coordinates due to lookahead. ## toolhead ```json @@ -98,9 +99,10 @@ The `toolhead` object reports state of the current tool: - `square_corner_velocity`: The currently set square corner velocity. This is the maximum velocity at which the tool may travel a 90 degree corner. -Note: `max_velocity`, `max_accel`, `max_accel_to_decel`, and -`square_corner_velocity` can be changed by the `SET_VELOCITY_LIMIT` gcode. -`M204` can also change `max_accel`. +!!! tip + `max_velocity`, `max_accel`, `max_accel_to_decel`, and + `square_corner_velocity` can be changed by the `SET_VELOCITY_LIMIT` gcode. + `M204` can also change `max_accel`. ## configfile ```json @@ -128,8 +130,9 @@ The `configfile` object reports printer configuration state: ## extruder *Enabled when `[extruder]` is included in printer.cfg* -Note: If multiple extruders are configured, extruder 0 is available as -`extruder`, extruder 1 as `extruder1` and so on. +!!! note + If multiple extruders are configured, extruder 0 is available as + `extruder`, extruder 1 as `extruder1` and so on. ```json { "temperature": 0.0, @@ -219,9 +222,10 @@ The `virtual_sdcard` object reports the state of the virtual sdcard: - `file_position`: The current file position in bytes. This will always be an integer value -**Note: `progress` and `file_position` will persist after a print has -paused, completed, or errored. They are cleared when the user issues -a SDCARD_RESET_FILE gcode or when a new print has started.** +!!! Note + `progress` and `file_position` will persist after a print has + paused, completed, or errored. They are cleared when the user issues + a SDCARD_RESET_FILE gcode or when a new print has started. ## print_stats *Enabled when `[virtual_sdcard]` is included in printer.cfg* @@ -252,8 +256,9 @@ The `print_stats` object reports `virtual_sdcard` print state: - `message`: If an error is detected, this field contains the error message generated. Otherwise it will be a null string. -**Note: After a print has started all of the values above will persist until -the user issues a SDCARD_RESET_FILE gcode or when a new print has started.** +!!! Note + After a print has started all of the values above will persist until + the user issues a SDCARD_RESET_FILE gcode or when a new print has started. ## display_status *Enabled when `[display]` or `[display_status]` is included in printer.cfg* @@ -360,8 +365,9 @@ The `bed_mesh` printer object reports the following state: - `mesh_matrix`: A 2 dimension array representing the interpolated mesh. If no matrix has been generated the result is `[[]]`. -**Note: See [web_api.md](web_api.md##bed-mesh-coordinates) for an example -of how to use this information to generate (X,Y,Z) coordinates.** +!!! tip + See [web_api.md](web_api.md##bed-mesh-coordinates) for an example + of how to use this information to generate (X,Y,Z) coordinates. ## gcode_macro macro_name *Enabled when `[gcode_macro macro_name]` is included in printer.cfg. diff --git a/docs/web_api.md b/docs/web_api.md index 9fbeb28..ec174b6 100644 --- a/docs/web_api.md +++ b/docs/web_api.md @@ -253,7 +253,8 @@ JSON-RPC request: "id": 4654 } ``` -**Note: A `null` value will fetch all available attributes for its key. +!!! note + A `null` value will fetch all available attributes for its key. Returns: @@ -289,11 +290,12 @@ HTTP request: ```http POST /printer/objects/subscribe?connection_id=123456789&gcode_move&extruder` ``` -**Note: The HTTP API requires that a `connection_id` is passed via the query -string or as part of the form. This should be the -[ID reported](#get-websocket-id) from a currently connected websocket. A -request that includes only the `connection_id` argument will cancel the -subscription on the specified websocket.** +!!! note + The HTTP API requires that a `connection_id` is passed via the query + string or as part of the form. This should be the + [ID reported](#get-websocket-id) from a currently connected websocket. A + request that includes only the `connection_id` argument will cancel the + subscription on the specified websocket. JSON-RPC request: ```json @@ -309,8 +311,9 @@ JSON-RPC request: "id": 5434 } ``` -**Note: If `objects` is set to an empty object then the subscription will -be cancelled.** +!!! note + If `objects` is set to an empty object then the subscription will + be cancelled. Returns: @@ -392,7 +395,7 @@ An object containing various fields that report server state. { "klippy_connected": true, "klippy_state": "ready", - "plugins": [ + "components": [ "database", "file_manager", "klippy_apis", @@ -405,17 +408,22 @@ An object containing various fields that report server state. "update_manager", "power" ], - "failed_plugins": [], + "failed_components": [], "registered_directories": ["config", "gcodes", "config_examples", "docs"] } ``` +!!! warning + This object also includes `plugins` and `failed_plugins` fields that + are now deprecated. They duplicate the information in + `components` and `failed_components`, and will be removed in the future. + Note that `klippy_state` will match the `state` value received from `/printer/info`. The `klippy_connected` item tracks the state of the -unix domain socket connect to Klippy. The `plugins` key will return a list of -enabled plugins. This can be used by clients to check if an optional -plugin is available. Optional plugins that do not load correctly will not -prevent the server from starting, thus any plugins that failed to load will be -reported in the `failed_plugins` field. +unix domain socket connect to Klippy. The `components` key will return a list +of enabled components. This can be used by clients to check if an optional +component is available. Optional components that do not load correctly will +not prevent the server from starting, thus any components that failed to load +will be reported in the `failed_components` field. #### Get Server Configuration HTTP request: @@ -987,12 +995,13 @@ JSON-RPC request: "id": 4644 } ``` +!!! tip + If the `root` argument is omitted the request will default to + the `gcodes` root. -**Note: If the `root` argument is omitted the request will default to -the `gcodes` root.** - -**Note: The `gcodes` root will only return files with valid gcode -extensions.** +!!! note + The `gcodes` root will only return files with valid gcode + extensions. Returns: A list of objects, where each object contains file data. @@ -1093,13 +1102,15 @@ modified time, and size. "filename": "3DBenchy_0.15mm_PLA_MK3S_2h6m.gcode" } ``` -**Note: The `print_start_time` and `job_id` fields are initialized to -`null`. They will be updated for each print job if the user has the -`[history]` plugin configured** +!!! note + The `print_start_time` and `job_id` fields are initialized to + `null`. They will be updated for each print job if the user has the + `[history]` component configured -**Note: The `data` field for each thumbnail is deprecated and will be removed -in a future release. Clients should retrieve the png directly using the -`relative_path` field.** +!!! warning + The `data` field for each thumbnail is deprecated and will be removed + in a future release. Clients should retrieve the png directly using the + `relative_path` field. #### Get directory information Returns a list of files and subdirectories given a supplied path. @@ -1124,9 +1135,9 @@ JSON-RPC request: "id": 5644 } ``` - -**Note: If the `path` argument is omitted then the command will return -directory information from the `gcodes` root.** +!!! tip + If the `path` argument is omitted then the command will return + directory information from the `gcodes` root. The `extended` argument is optional and defaults to false. If supplied and set to true then data returned for gcode files @@ -1223,8 +1234,9 @@ JSON-RPC request: "id": 6545 } ``` -**Note: If the specified directory contains files then the delete request -will fail unless the `force` argument is set to `true`.** +!!! warning + If the specified directory contains files then the delete request + will fail unless the `force` argument is set to `true`. Returns: @@ -1467,9 +1479,10 @@ as an array of strings, where each string references a nested field. This is useful for scenarios where your namespace contains keys that include a "." character. -**Note: Moonraker reserves the `moonraker`, `gcode_metadata`, and `history` -namespaces. Clients may read from these namespaces but they may not modify -them.** +!!! note + Moonraker reserves the `moonraker`, `gcode_metadata`, and `history` + namespaces. Clients may read from these namespaces but they may not + modify them. For example, assume the following object is stored in the "superclient" namespace: @@ -1568,10 +1581,11 @@ HTTP request: ```http POST /server/database/item?namespace={namespace}&key={key}value={value}` ``` -**Note: If the `value` is not a string type, the `value` argument must -provide a [type hint](#query-string-type-hints). Alternatively, -arguments may be passed via the request body in JSON format. For -example:** +!!! note + If the `value` is not a string type, the `value` argument must + provide a [type hint](#query-string-type-hints). Alternatively, + arguments may be passed via the request body in JSON format. For + example: ```http POST /server/database/item Content-Type: application/json @@ -1639,7 +1653,7 @@ deleted item. ``` ### Update Manager APIs -The following endpoints are available when the `[update_manager]` plugin has +The following endpoints are available when the `[update_manager]` component has been configured: #### Get update status @@ -1887,7 +1901,7 @@ Returns: `ok` when complete ### Power APIs -The APIs below are available when the `[power]` plugin has been configured. +The APIs below are available when the `[power]` component has been configured. #### Get Device List HTTP request: @@ -2240,7 +2254,7 @@ An object containing simulates Octoprint Printer profile ``` ### History APIs -The APIs below are avilable when the `[history]` plugin has been configured. +The APIs below are avilable when the `[history]` component has been configured. #### Get job list HTTP request: @@ -2346,9 +2360,9 @@ JSON-RPC request: "id": 5534 } ``` - -**Note: it is possible to replace the `uid` argument with `all=true` -to delete all jobs in the history database.** +!!! tip + It is possible to replace the `uid` argument with `all=true` + to delete all jobs in the history database. Returns: @@ -2571,9 +2585,10 @@ The websocket is located at `ws://host:port/websocket`, for example: var s = new WebSocket("ws://" + location.host + "/websocket"); ``` -**Note: A client using API Key authorization may request a -[oneshot token](#generate-a-oneshot-token), applying the result to the -websocket request's query string:** +!!! tip + A client using API Key authorization may request a + [oneshot token](#generate-a-oneshot-token), applying the result to the + websocket request's query string: ```http ws://host:port/websocket?token={32 character base32 string} diff --git a/mkdocs.yml b/mkdocs.yml index d1535ae..4c9d598 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -11,9 +11,11 @@ nav: - API Changes: api_changes.md - 'Backend Developers': - Contributing: contributing.md + - Components: components.md theme: name: readthedocs plugins: - search markdown_extensions: - codehilite + - admonition