Issue M112 through the restful API


#1

I was wondering if it is possible to issue an arbitrary command to the printer through the API.
I can't seem to find it in the documentation.
My objective is to be able to issue an M112 to abort a print immediately, since cancel seems to cease the streaming of commands.


#2

Sounds like you want the Terminal tab of OctoPrint.


#3

I want to use OctoPrint's restful API to issue an arbitrary command (in this case an M112), going to the terminal tab is not an option since I want to do it programatically (if that word exists) :slight_smile:


#4

Have you tried the ability to send an arbitrary command to the printer?

POST /api/printer/command HTTP/1.1
Host: example.com
Content-Type: application/json
X-Api-Key: abcdef...

{
"command": "M106"
}


#5

That's what I needed and could not find :smiley: thanks.
I will give it a go.


#6

Here is the documentation I could not find...


#7

Ah. That would be more difficult.

Here's my attempt at "going to school" on what we're staring at in the screenshot:

octoprint/templates/terminal.ninja2

            <input type="text" id="terminal-command" data-bind="value: command, event: { keyup: function(d,e) { return handleKeyUp(e); }, keydown: function(d,e) { return handleKeyDown(e); } }, enable: isOperational() && loginState.isUser()" autocomplete="off">
            <a class="btn add-on" id="terminal-send" data-bind="click: sendCommand, enable: isOperational() && loginState.isUser()">{{ _('Send') }}</a>

From this, we know that a JavaScript function called sendCommand() is involved but finding it directly would be problematic. Instead, I like using GitHub for that, as in this search. So now, we know the location of the client-side JavaScript.

octoprint/static/js/app/viewmodels/terminal.js

        self.sendCommand = function() {
            var command = self.command();
            if (!command) {
                return;
            }

            var commandToSend = command;
            var commandMatch = commandToSend.match(commandRe);
            if (commandMatch !== null) {
                var fullCode = commandMatch[1].toUpperCase(); // full code incl. sub code
                var mainCode = commandMatch[2].toUpperCase(); // main code only without sub code

                if (self.blacklist.indexOf(mainCode) < 0 && self.blacklist.indexOf(fullCode) < 0) {
                    // full or main code not on blacklist -> upper case the whole command
                    commandToSend = commandToSend.toUpperCase();
                } else {
                    // full or main code on blacklist -> only upper case that and leave parameters as is
                    commandToSend = fullCode + (commandMatch[4] !== undefined ? commandMatch[4] : "");
                }
            }

            if (commandToSend) {
                OctoPrint.control.sendGcode(commandToSend)
                    .done(function() {
                        self.cmdHistory.push(command);
                        self.cmdHistory.slice(-300); // just to set a sane limit to how many manually entered commands will be saved...
                        self.cmdHistoryIdx = self.cmdHistory.length;
                        self.command("");
                    });
            }
        };

It looks like this is the MOP ("meat and potatoes") of what's going on.

            if (commandToSend) {
                OctoPrint.control.sendGcode(commandToSend)
                    .done(function() {
                        self.cmdHistory.push(command);
                        self.cmdHistory.slice(-300); // just to set a sane limit to how many manually entered commands will be saved...
                        self.cmdHistoryIdx = self.cmdHistory.length;
                        self.command("");
                    });
            }

Hopefully this helps.


#8

Thanks for the reply!
But @Derek's solution is exactly what I wanted.
By the way, I tried, and it does exactly what I want.
Thanks!


#9

But then again, there's the formal OctoPrint API itself which has this.

POST /api/printer/command

UPDATE: Oh man, he beat me to it.


#10

I am trying to do exactly this, but in Python, so far I'm a day into it and... while I've been able to get data out of the API, I have not been able to "POST" a request, the returned message (and the message in the logfile) is "ERROR - 500 POST"... and my command URL back... I am a complete Novice in both python and http so much is unclear - any input appreciated as to the correct layout for an arbitrary command via python. I am using "import requests" under python 3.5. I am trying to execute M114 and capture the X, Y, Z position.

TIA for any input. -James

[Edit 1]
Since there are only 3 posts available for newbs, I'm editing additional info into the existing posts:
As stated; I'm attempting to retrieve various information from the API, this is a "journey of discovery" since my expertise lies in areas other than coding and data parsing.

But the gist is:
pull operational information from the API,
reformat as needed,
push said info into a tcp sockey on another tool
(MTConnect) which then publishes XML in a strictly formatted framework,
All this makes for a generic interface that other machines can watch and react to, leading toward emergent scheduling and a host of other benefits.
For context, linuxCNC and ROS will be using the same output format so that all three industrial control packages can be managed entirely by automation.and interact with one another via these status driven interfaces. its all intended to be an open source management "OS" for clusters of machines: More detailed info is here http://patreon.com/CubeSpawn TIA to all that are helping solve this! Viva la (robot) revolucion!


#11

In the code where you're about to POST to the API, you must include the printer's API key in the header information.

Again, the documentation is useful in cases like this.


#12

Which I have done, although it has been the less desired version embedded in the URL, since every permutation of embedding it in the "header" statement has resulted in a variation of an "API key missing" error

this exact same code with a slightly modified URL returns data fine with a "get" request

I am SURE I am passing the parameters wrong, but don't know what right would look like :wink:

the two links limit induced me to add the extra spaces in the urls

I have visited this page many dozen times: h t t p : / / d o c s . octoprint.org/en/master/api/printer.html#send-an-arbitrary-command-to-the-printer

and these: (+ many others)
h t t p s : / / g i t h u b . com/Ignisleo/OctoprintPythonAPI/blob/master/octoprint_api.py
h t t p s : / / g i t h u b . com/dragondgold/octo-control/blob/master/octo-control.py
h t t p s : / / g i t h u b . com/mtconnect/PocketNC_adapter

Here is the python:

import requests

parameters = {"command": "M114"}

r = requests.post("h t t p : / / 192.168.0.10/api/printer/command?apikey=8A1AF666EE3848A4845E7401E8A0E786", params=parameters)

print(r.headers)
print(r.content)
print(r.text)

Here is the output
I inserted extra spaces in the H1 tags and the title tags to bypass the markup engine

/home/cubespawn/PycharmProjects/Octoprint/venv/bin/python /home/cubespawn/PycharmProjects/Octoprint/APIComms.py
{'X-Robots-Tag': 'noindex, nofollow, noimageindex', 'Content-Type': 'text/html', 'Content-Length': '291'}
b'\n< title > 500 Internal Server Error<
title>\n< h1>Internal Server Error</ h1>\n

The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.< /p>\n'

< title>500 Internal Server Error< /title>
< h1>Internal Server Error< /h1>

The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.

Process finished with exit code 0

and here is the log:

018-05-23 06:10:23,225 - octoprint - ERROR - Exception on /api/printer/command [POST]
Traceback (most recent call last):
File "/home/pi/oprint/local/lib/python2.7/site-packages/Flask-0.10.1-py2.7.egg/flask/app.py", line 1817, in wsgi_app
response = self.full_dispatch_request()
File "/home/pi/oprint/local/lib/python2.7/site-packages/Flask-0.10.1-py2.7.egg/flask/app.py", line 1477, in full_dispatch_request
rv = self.handle_user_exception(e)
File "/home/pi/oprint/local/lib/python2.7/site-packages/Flask-0.10.1-py2.7.egg/flask/app.py", line 1381, in handle_user_exception
reraise(exc_type, exc_value, tb)
File "/home/pi/oprint/local/lib/python2.7/site-packages/Flask-0.10.1-py2.7.egg/flask/app.py", line 1475, in full_dispatch_request
rv = self.dispatch_request()
File "/home/pi/oprint/local/lib/python2.7/site-packages/Flask-0.10.1-py2.7.egg/flask/app.py", line 1461, in dispatch_request
return self.view_functionsrule.endpoint
File "/home/pi/oprint/local/lib/python2.7/site-packages/OctoPrint-1.3.8-py2.7.egg/octoprint/server/util/flask.py", line 1132, in decorated_view
return flask_login.login_required(func)(*args, **kwargs)
File "/home/pi/oprint/local/lib/python2.7/site-packages/Flask_Login-0.2.11-py2.7.egg/flask_login.py", line 758, in decorated_view
return func(*args, **kwargs)
File "/home/pi/oprint/local/lib/python2.7/site-packages/OctoPrint-1.3.8-py2.7.egg/octoprint/server/api/printer.py", line 333, in printerCommand
if not "application/json" in request.headers["Content-Type"]:
File "/home/pi/oprint/local/lib/python2.7/site-packages/Werkzeug-0.8.3-py2.7.egg/werkzeug/datastructures.py", line 1228, in _ getitem _
return self.environ[key]
KeyError: 'CONTENT_TYPE'
2018-05-23 06:10:23,261 - tornado.access - ERROR - 500 POST /api/printer/command?apikey=8A1AF666EE3848A4845E7401E8A0E786&command=M114 (127.0.0.1) 82.97ms


#13

I'm also using python, try using the data and headers of the requests post.

import json
import requests

host = '127.0.0.1'
port = 5000
endpoint = 'printer/command'
data = json.dumps({
    'command': 'M112',
})
key = 'ABC123'

headers = {
    'Content-type': 'application/json',
    'X-Api-Key': key,
}
r = requests.post(
    url="http://{host}:{port}/api/{endpoint}".format(
        host=host,
        port=port,
        endpoint=endpoint
    ),
    data=data,
    headers=headers
)

if you want to send several commands one after the other replace the data with:

{
    'commands': [
        'G28',
        'M112',
    ]
}

for example.


#14

Hi Miguel,
I'll give your code structure a whirl, meanwhile I see a different problem staring me in the eye:
I am using the PyCharm IDE which in turn is supposed to have constructed a venv environment for the python 3.5 interpreter, but if that were so, i should not have a bunch of 2.7 references in the log, I saw it right after I posted last night, but was too tired out to address it 'till this morning...

WHOOPIE!!!, I'll spend time troubleshooting the tools, rather than using them,... Ain't technology fun!?
once thats sorted I'll drive your code examples to victory!... er further away from infamy, at least. :wink:
-James


#15

So, using your exact code with my particulars, I get a 204 return code

looking for verbose output I added

print(r)
print(r.headers)
print(r.content)
print(r.text)

and I get this output
/home/cubespawn/PycharmProjects/Octoprint/venv/bin/python /home/cubespawn/PycharmProjects/Octoprint/APIComms.py
<Response [204]>
{'X-Robots-Tag': 'noindex, nofollow, noimageindex', 'Expires': '-1', 'X-Clacks-Overhead': 'GNU Terry Pratchett', 'Cache-Control': 'pre-check=0, no-cache, no-store, must-revalidate, post-check=0, max-age=0', 'Set-Cookie': 'session_P80=.eJyrVopPK0otzlCyKikqTdVRis9MUbKqVlJIUrJSiqxyNI0MD62MCvesisz1rPANjzTxdck29Hdxy4jKjcqMrHI1iArxyohySbZVqtVRykxJzSvJLKnUSywtyYgvqSxIVbLKK83JQZJBMj3CyK08MdAWrLO0OLUoHqtcLQDT-jRj.Deb6Bg.Fn-jvcTZBFm1CplSf1qOvVYvI1U; Path=/; HttpOnly', 'Pragma': 'no-cache', 'Content-Length': '0', 'Content-Type': 'text/html; charset=utf-8'}
b''
Process finished with exit code 0

Clearly much more on target than the previous attempts, many thanks!

but still no X, Y, Z coordinates, as i would receive from the terminal, so the mystery persists... :wink:

[Edit]
Damn! it does output the M114.... TO THE TERMINAL!!! which is probably the desired behavior, but does me NO good... hmmmmm - your code solved the problem, but I must be asking the wrong question.....

[Edit 2]
I turned on the Serial log and sure enough, the data shows up there. although on one hand, thats a logical way to behave I'm not sure its very useful, since when I ask for data from the API, i'd sort of expect the data to be retrievable from said API, and building a workaround by getting some data from API responses, while parsing other data from logs will be problematic at best... my lack of python skill is not matched by a general lack of system design skills in this instance...:wink:

[Edit 3]
@miguel @foosel I'm responding to the post below this one, rather than a specific endpoint for toolhead coordinates, wouldn't a generic endpoint for terminal commands (or data producing commands) cover more ground? since the goal of automation is to have the same capabilities available via a programmatic interface that are available via other ones. Does this merit a feature request? I'd assume that the API would eventually converge toward this solution as all the edge conditions get identified in differing control scenarios.anyhow, so maybe now is the time to begin that process... :wink: or maybe its too much, :wink:

[Edit 4}
@foosel, many thanks for the clarifications. Based on further reading and your direction, I'm leaning toward adding a custom log to the logging plugin for now, that will capture all the anticipated fields and uses the existing infrastructure, I can parse and delete the log at regular intervals to extract the needed info. That'll keep me out of any functional conflicts with blocking the print controller, and keep the size under control. Thanks for your help!


#16

Sorry, I skimmed through your post and did not read what was your end objective, I wrongly assumed you were just trying to issue an M112 (my original issue) when in fact it was nothing of the sort.

I think the correct way to do that would be to create a GET /api/printer/printhead endpoint to return the current printhead status (including position) since POST /api/printer/printhead allows you to jog to a position. @foosel can you give us a hint here on what would be the right approach to get the toolhead coordinates?

I actually also need to poll the terminal for a return from the printer and still don't know how to do it, I'm relying on GETs to some of the API's endpoints to retrieve data (temperatures for eg.).


#17

Replying to your Edit 3, a generic command that would return a reply would need to block until it was handled by the printer and a reply was received by octoprint.
I don't know how feasible would it be since, if you are running a print job, it would take a while for the printer's buffer to flush and execute your command.
The only exception for this are the commands handled by the EMERGENCY_PARSER.


#18

I'll just answer this with a link:

You'll need to hook into the PositionUpdate event. OctoPrint will detect and parse M114 responses and fire off this event (with the parsed data). Due to the nature of the server architecture however it is not possible to provide an endpoint that would do something like send M114 and then block until the event is captured, since that would block all other communication with clients in the meantime.

Frontend or backend? In the frontend use the fromCurrentData callback. In the backend/via API, you'll need to subscribe to the push SockJS endpoint. The included client also includes code for that.


#19

Backend :slight_smile:
I'll give the included client a go (believe it or not I actually re-implemented everything for some wild reason).


#20

Alright, here's the problem. You're treating a POST as if it were a GET. And yes, this works great for the GET version by just including the key in the querystring like that.

r = requests.post("h t t p : / / 192.168.0.10/api/printer/command", params=parameters, headers={"X-Api-Key": "whatever"})