NodeODM 1.5.1 support, simplified task view, progress reporting via API

pull/663/head
Piero Toffanin 2019-05-20 10:05:56 -04:00
rodzic 9440fe2d44
commit 8263d060c0
14 zmienionych plików z 108 dodań i 374 usunięć

Wyświetl plik

@ -34,7 +34,7 @@ class ProcessingNodeFilter(FilterSet):
class Meta: class Meta:
model = ProcessingNode model = ProcessingNode
fields = ['has_available_options', 'id', 'hostname', 'port', 'api_version', 'queue_count', 'max_images', 'label', ] fields = ['has_available_options', 'id', 'hostname', 'port', 'api_version', 'queue_count', 'max_images', 'label', 'engine', 'engine_version', ]
class ProcessingNodeViewSet(viewsets.ModelViewSet): class ProcessingNodeViewSet(viewsets.ModelViewSet):
""" """

Wyświetl plik

@ -97,11 +97,9 @@ def boot():
def add_default_presets(): def add_default_presets():
try: try:
Preset.objects.update_or_create(name='Volume Analysis', system=True, Preset.objects.update_or_create(name='Volume Analysis', system=True,
defaults={'options': [{'name': 'use-opensfm-dense', 'value': True}, defaults={'options': [{'name': 'dsm', 'value': True},
{'name': 'dsm', 'value': True},
{'name': 'dem-resolution', 'value': '2'}, {'name': 'dem-resolution', 'value': '2'},
{'name': 'depthmap-resolution', 'value': '1000'}, {'name': 'depthmap-resolution', 'value': '1000'}]})
{'name': 'opensfm-depthmap-min-patch-sd', 'value': '0'}]})
Preset.objects.update_or_create(name='3D Model', system=True, Preset.objects.update_or_create(name='3D Model', system=True,
defaults={'options': [{'name': 'mesh-octree-depth', 'value': "11"}, defaults={'options': [{'name': 'mesh-octree-depth', 'value': "11"},
{'name': 'use-3dmesh', 'value': True}, {'name': 'use-3dmesh', 'value': True},
@ -126,7 +124,8 @@ def add_default_presets():
Preset.objects.update_or_create(name='Fast Orthophoto', system=True, Preset.objects.update_or_create(name='Fast Orthophoto', system=True,
defaults={'options': [{'name': 'fast-orthophoto', 'value': True}]}) defaults={'options': [{'name': 'fast-orthophoto', 'value': True}]})
Preset.objects.update_or_create(name='High Resolution', system=True, Preset.objects.update_or_create(name='High Resolution', system=True,
defaults={'options': [{'name': 'dsm', 'value': True}, defaults={'options': [{'name': 'ignore-gsd', 'value': True},
{'name': 'dsm', 'value': True},
{'name': 'depthmap-resolution', 'value': '1000'}, {'name': 'depthmap-resolution', 'value': '1000'},
{'name': 'dem-resolution', 'value': "2.0"}, {'name': 'dem-resolution', 'value': "2.0"},
{'name': 'orthophoto-resolution', 'value': "2.0"}, {'name': 'orthophoto-resolution', 'value': "2.0"},

Wyświetl plik

@ -175,22 +175,7 @@ class Task(models.Model):
(pending_actions.IMPORT, 'IMPORT'), (pending_actions.IMPORT, 'IMPORT'),
) )
# Not an exact science TASK_PROGRESS_LAST_VALUE = 0.85
TASK_OUTPUT_MILESTONES_LAST_VALUE = 0.85
TASK_OUTPUT_MILESTONES = {
'Running ODM Load Dataset Cell': 0.01,
'Running ODM Load Dataset Cell - Finished': 0.05,
'opensfm/bin/opensfm match_features': 0.10,
'opensfm/bin/opensfm reconstruct': 0.20,
'opensfm/bin/opensfm export_visualsfm': 0.30,
'Running ODM Meshing Cell': 0.50,
'Running MVS Texturing Cell': 0.55,
'Running ODM Georeferencing Cell': 0.60,
'Running ODM DEM Cell': 0.70,
'Running ODM Orthophoto Cell': 0.75,
'Running ODM OrthoPhoto Cell - Finished': 0.80,
'Compressing all.zip:': TASK_OUTPUT_MILESTONES_LAST_VALUE
}
id = models.UUIDField(primary_key=True, default=uuid_module.uuid4, unique=True, serialize=False, editable=False) id = models.UUIDField(primary_key=True, default=uuid_module.uuid4, unique=True, serialize=False, editable=False)
@ -578,22 +563,18 @@ class Task(models.Model):
# Need to update status (first time, queued or running?) # Need to update status (first time, queued or running?)
if self.uuid and self.status in [None, status_codes.QUEUED, status_codes.RUNNING]: if self.uuid and self.status in [None, status_codes.QUEUED, status_codes.RUNNING]:
# Update task info from processing node # Update task info from processing node
info = self.processing_node.get_task_info(self.uuid) current_lines_count = len(self.console_output.split("\n"))
info = self.processing_node.get_task_info(self.uuid, current_lines_count)
self.processing_time = info.processing_time self.processing_time = info.processing_time
self.status = info.status.value self.status = info.status.value
current_lines_count = len(self.console_output.split("\n")) if len(info.output) > 0:
console_output = self.processing_node.get_task_console_output(self.uuid, current_lines_count) self.console_output += "\n".join(info.output) + '\n'
if len(console_output) > 0:
self.console_output += "\n".join(console_output) + '\n'
# Update running progress # Update running progress
for line in console_output: self.running_progress = (info.progress / 100.0) * self.TASK_PROGRESS_LAST_VALUE
for line_match, value in self.TASK_OUTPUT_MILESTONES.items():
if line_match in line:
self.running_progress = value
break
if info.last_error != "": if info.last_error != "":
self.last_error = info.last_error self.last_error = info.last_error
@ -625,7 +606,7 @@ class Task(models.Model):
if time_has_elapsed or int(progress) == 100: if time_has_elapsed or int(progress) == 100:
Task.objects.filter(pk=self.id).update(running_progress=( Task.objects.filter(pk=self.id).update(running_progress=(
self.TASK_OUTPUT_MILESTONES_LAST_VALUE + (float(progress) / 100.0) * 0.1)) self.TASK_PROGRESS_LAST_VALUE + (float(progress) / 100.0) * 0.1))
last_update = time.time() last_update = time.time()
while not extracted: while not extracted:

Wyświetl plik

@ -1,235 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import '../css/BasicTaskView.scss';
import update from 'immutability-helper';
import PipelineSteps from '../classes/PipelineSteps';
import StatusCodes from '../classes/StatusCodes';
import $ from 'jquery';
class BasicTaskView extends React.Component {
static defaultProps = {};
static propTypes = {
source: PropTypes.oneOfType([
PropTypes.string.isRequired,
PropTypes.func.isRequired
]),
taskStatus: PropTypes.number,
onAddLines: PropTypes.func
};
constructor(props){
super();
this.state = {
lines: [],
currentRf: 0,
rf: PipelineSteps.get(),
loaded: false
};
this.imageUpload = {
action: "imageupload",
label: "Image Resize / Upload",
icon: "fa fa-image"
};
// Add a post processing step
this.state.rf.push({
action: "postprocessing",
label: "Post Processing",
icon: "fa fa-file-archive-o",
beginsWith: "Running ODM OrthoPhoto Cell - Finished",
endsWith: null
});
this.addLines = this.addLines.bind(this);
this.setupDynamicSource = this.setupDynamicSource.bind(this);
this.reset = this.reset.bind(this);
this.tearDownDynamicSource = this.tearDownDynamicSource.bind(this);
this.getRfEndStatus = this.getRfEndStatus.bind(this);
this.getRfRunningStatus = this.getRfRunningStatus.bind(this);
this.suffixFor = this.suffixFor.bind(this);
this.updateRfState = this.updateRfState.bind(this);
this.getInitialStatus = this.getInitialStatus.bind(this);
}
componentDidMount(){
this.reset();
}
setupDynamicSource(){
const updateFromSource = () => {
let sourceUrl = typeof this.props.source === 'function' ?
this.props.source(this.state.lines.length) :
this.props.source;
// Fetch
this.sourceRequest = $.get(sourceUrl, text => {
if (text !== ""){
let lines = text.split("\n");
this.addLines(lines);
}
})
.always((_, textStatus) => {
if (textStatus !== "abort" && this.props.refreshInterval !== undefined){
this.sourceTimeout = setTimeout(updateFromSource, this.props.refreshInterval);
}
if (!this.state.loaded) this.setState({loaded: true});
});
};
updateFromSource();
}
getRfEndStatus(){
return this.props.taskStatus === StatusCodes.COMPLETED ?
'completed' :
'errored';
}
getRfRunningStatus(){
return [StatusCodes.RUNNING, StatusCodes.QUEUED].indexOf(this.props.taskStatus) !== -1 ?
'running' :
'errored';
}
getInitialStatus(){
if ([null, StatusCodes.QUEUED, StatusCodes.RUNNING].indexOf(this.props.taskStatus) !== -1){
return 'queued';
}else{
return this.getRfEndStatus();
}
}
reset(){
this.state.rf.forEach(p => {
p.state = this.getInitialStatus();
});
if ([StatusCodes.RUNNING].indexOf(this.props.taskStatus) !== -1){
this.state.rf[0].state = 'running';
}
this.tearDownDynamicSource();
this.setState({lines: [], currentRf: 0, loaded: false, rf: this.state.rf});
this.setupDynamicSource();
}
tearDownDynamicSource(){
if (this.sourceTimeout) clearTimeout(this.sourceTimeout);
if (this.sourceRequest) this.sourceRequest.abort();
}
componentDidUpdate(prevProps){
let taskFailed;
let taskCompleted;
let taskRestarted;
taskFailed = [StatusCodes.FAILED, StatusCodes.CANCELED].indexOf(this.props.taskStatus) !== -1;
taskCompleted = this.props.taskStatus === StatusCodes.COMPLETED;
if (prevProps.taskStatus !== this.props.taskStatus){
taskRestarted = this.props.taskStatus === null;
}
this.updateRfState(taskFailed, taskCompleted, taskRestarted);
}
componentWillUnmount(){
this.tearDownDynamicSource();
}
addLines(lines){
if (!Array.isArray(lines)) lines = [lines];
let currentRf = this.state.currentRf;
const updateRf = (rfIndex, line) => {
const current = this.state.rf[rfIndex];
if (!current) return;
if (current.beginsWith && line.endsWith(current.beginsWith)){
current.state = this.getRfRunningStatus();
// Set previous as done
if (this.state.rf[rfIndex - 1]){
this.state.rf[rfIndex - 1].state = 'completed';
}
}else if (current.endsWith && line.endsWith(current.endsWith)){
current.state = this.getRfEndStatus();
// Check next
updateRf(rfIndex + 1, line);
currentRf = rfIndex + 1;
}
};
lines.forEach(line => {
updateRf(currentRf, line);
});
this.setState(update(this.state, {
lines: {$push: lines}
}));
this.setState({
rf: this.state.rf,
currentRf
});
this.updateRfState();
if (this.props.onAddLines) this.props.onAddLines(lines);
}
updateRfState(taskFailed, taskCompleted, taskRestarted){
// If the task has just failed, update all items that were either running or in queued state
if (taskFailed){
this.state.rf.forEach(p => {
if (p.state === 'queued' || p.state === 'running') p.state = this.getInitialStatus();
});
}
// If completed, all steps must have completed
if (taskCompleted){
this.state.rf.forEach(p => p.state = 'completed');
}
if (taskRestarted){
this.state.rf.forEach(p => p.state = 'queued');
}
}
suffixFor(state){
if (state === 'running'){
return (<span>...</span>);
}else if (state === 'completed'){
return (<i className="fa fa-check"></i>);
}else if (state === 'errored'){
return (<i className="fa fa-times"></i>);
}
}
render() {
const { rf, loaded } = this.state;
const imageUploadState = this.props.taskStatus === null
? 'running'
: 'completed';
return (<div className={"basic-task-view " + (loaded ? 'loaded' : '')}>
<div className={imageUploadState + " processing-step"}>
<i className={this.imageUpload.icon + " fa-fw"}></i> {this.imageUpload.label} {this.suffixFor(imageUploadState)}
</div>
{rf.map(p => {
const state = loaded ? p.state : 'queued';
return (<div key={p.action} className={state + " processing-step"}>
<i className={p.icon + " fa-fw"}></i> {p.label} {this.suffixFor(state)}
</div>);
})}
</div>);
}
}
export default BasicTaskView;

Wyświetl plik

@ -10,7 +10,6 @@ import HistoryNav from '../classes/HistoryNav';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import TaskPluginActionButtons from './TaskPluginActionButtons'; import TaskPluginActionButtons from './TaskPluginActionButtons';
import PipelineSteps from '../classes/PipelineSteps'; import PipelineSteps from '../classes/PipelineSteps';
import BasicTaskView from './BasicTaskView';
import Css from '../classes/Css'; import Css from '../classes/Css';
class TaskListItem extends React.Component { class TaskListItem extends React.Component {
@ -468,33 +467,33 @@ class TaskListItem extends React.Component {
expanded = ( expanded = (
<div className="expanded-panel"> <div className="expanded-panel">
<div className="row"> <div className="row">
<div className="col-md-3 no-padding"> <div className="col-md-12 no-padding">
<div className="labels"> <div className="console-switch text-right pull-right">
<strong>Created on: </strong> {(new Date(task.created_at)).toLocaleString()}<br/> <div className="console-output-label">Task Output: </div><ul className="list-inline">
<li>
<div className="btn-group btn-toggle">
<button onClick={this.setView("console")} className={"btn btn-xs " + (this.state.view === "basic" ? "btn-default" : "btn-primary")}>On</button>
<button onClick={this.setView("basic")} className={"btn btn-xs " + (this.state.view === "console" ? "btn-default" : "btn-primary")}>Off</button>
</div>
</li>
</ul>
</div> </div>
<div className="labels">
<strong>Processing Node: </strong> {task.processing_node_name || "-"} ({task.auto_processing_node ? "auto" : "manual"})<br/> <div className="mb">
</div> <div className="labels">
{Array.isArray(task.options) ? <strong>Created on: </strong> {(new Date(task.created_at)).toLocaleString()}<br/>
<div className="labels">
<strong>Options: </strong> {this.optionsToList(task.options)}<br/>
</div> </div>
: ""} <div className="labels">
{/* TODO: List of images? */} <strong>Processing Node: </strong> {task.processing_node_name || "-"} ({task.auto_processing_node ? "auto" : "manual"})<br/>
</div>
{showOrthophotoMissingWarning ? {Array.isArray(task.options) ?
<div className="task-warning"><i className="fa fa-warning"></i> <span>An orthophoto could not be generated. To generate one, make sure GPS information is embedded in the EXIF tags of your images, or use a Ground Control Points (GCP) file.</span></div> : ""} <div className="labels">
<strong>Options: </strong> {this.optionsToList(task.options)}<br/>
</div> </div>
<div className="col-md-9"> : ""}
<div className="switch-view text-right pull-right"> {/* TODO: List of images? */}
<i className="fa fa-list-ul"></i> <a href="javascript:void(0);" onClick={this.setView("basic")} </div>
className={this.state.view === 'basic' ? "selected" : ""}>Simple</a>
|
<i className="fa fa-desktop"></i> <a href="javascript:void(0);" onClick={this.setView("console")}
className={this.state.view === 'console' ? "selected" : ""}>Console</a>
</div>
{this.state.view === 'console' ? {this.state.view === 'console' ?
<Console <Console
className="floatfix" className="floatfix"
@ -508,14 +507,8 @@ class TaskListItem extends React.Component {
maximumLines={500} maximumLines={500}
/> : ""} /> : ""}
{this.state.view === 'basic' ? {showOrthophotoMissingWarning ?
<BasicTaskView <div className="task-warning"><i className="fa fa-warning"></i> <span>An orthophoto could not be generated. To generate one, make sure GPS information is embedded in the EXIF tags of your images, or use a Ground Control Points (GCP) file.</span></div> : ""}
source={this.consoleOutputUrl}
ref={domNode => this.basicView = domNode}
refreshInterval={this.shouldRefresh() ? 3000 : undefined}
onAddLines={this.checkForCommonErrors}
taskStatus={task.status}
/> : ""}
{showMemoryErrorWarning ? {showMemoryErrorWarning ?
<div className="task-warning"><i className="fa fa-support"></i> <span>It looks like your processing node ran out of memory. If you are using docker, make sure that your docker environment has <a href={memoryErrorLink} target="_blank">enough RAM allocated</a>. Alternatively, make sure you have enough physical RAM, reduce the number of images, make your images smaller, or reduce the max-concurrency parameter from the task's <a href="javascript:void(0);" onClick={this.startEditing}>options</a>. You can also try to use a <a href="https://www.opendronemap.org/webodm/lightning/" target="_blank">cloud processing node</a>.</span></div> : ""} <div className="task-warning"><i className="fa fa-support"></i> <span>It looks like your processing node ran out of memory. If you are using docker, make sure that your docker environment has <a href={memoryErrorLink} target="_blank">enough RAM allocated</a>. Alternatively, make sure you have enough physical RAM, reduce the number of images, make your images smaller, or reduce the max-concurrency parameter from the task's <a href="javascript:void(0);" onClick={this.startEditing}>options</a>. You can also try to use a <a href="https://www.opendronemap.org/webodm/lightning/" target="_blank">cloud processing node</a>.</span></div> : ""}

Wyświetl plik

@ -1,10 +0,0 @@
import React from 'react';
import { mount } from 'enzyme';
import BasicTaskView from '../BasicTaskView';
describe('<BasicTaskView />', () => {
it('renders without exploding', () => {
const wrapper = mount(<BasicTaskView source="http://localhost/output" taskStatus={40} />);
expect(wrapper.exists()).toBe(true);
})
});

Wyświetl plik

@ -1,38 +0,0 @@
.basic-task-view{
margin-bottom: 8px;
opacity: 0.7;
&.loaded{
opacity: 1;
}
.processing-step{
opacity: 0.7;
&.completed{
opacity: 1;
/* font-weight: bold; */
}
&.running{
opacity: 1;
animation: pulse ease-in-out 1.4833s infinite;
}
&.queued{
}
&.errored{
opacity: 1;
}
}
@keyframes pulse {
0% {
opacity: 0.7;
}
50% {
opacity: 1;
}
100% {
opacity: 0.7;
}
}
}

Wyświetl plik

@ -57,6 +57,8 @@
.task-warning{ .task-warning{
margin-top: 16px; margin-top: 16px;
margin-bottom: 16px;
i.fa.fa-warning{ i.fa.fa-warning{
color: #ff8000; color: #ff8000;
} }
@ -100,25 +102,20 @@
display: inline; display: inline;
} }
.switch-view{ .console-switch{
margin-right: 10px;
margin-bottom: 8px; margin-bottom: 8px;
font-size: 90%;
position: relative; position: relative;
z-index: 99; z-index: 99;
.console-output-label{
a{
margin-right: 8px; margin-right: 8px;
&.selected{
font-weight: bold;
}
&:last-child{
margin-right: 0;
}
} }
.console-output-label, ul{
i{ display: inline-block;
margin-left: 8px;
} }
} }
.mb{
margin-bottom: 12px;
}
} }

Wyświetl plik

@ -20,8 +20,12 @@
<td>{{ processing_node.api_version }}</td> <td>{{ processing_node.api_version }}</td>
</tr> </tr>
<tr> <tr>
<td>{% trans "ODM Version" %}</td> <td>{% trans "Engine" %}</td>
<td>{{ processing_node.odm_version }}</td> <td>{{ processing_node.engine }}</td>
</tr>
<tr>
<td>{% trans "Engine Version" %}</td>
<td>{{ processing_node.engine_version }}</td>
</tr> </tr>
<tr> <tr>
<td>{% trans "Queue Count" %}</td> <td>{% trans "Queue Count" %}</td>

Wyświetl plik

@ -355,8 +355,11 @@ class TestApi(BootTestCase):
# Verify max images field # Verify max images field
self.assertTrue("max_images" in res.data) self.assertTrue("max_images" in res.data)
# Verify odm version # Verify engine version
self.assertTrue("odm_version" in res.data) self.assertTrue("engine_version" in res.data)
# Verify engine
self.assertTrue("engine" in res.data)
# label should be hostname:port (since no label is set) # label should be hostname:port (since no label is set)
self.assertEqual(res.data['label'], pnode.hostname + ":" + str(pnode.port)) self.assertEqual(res.data['label'], pnode.hostname + ":" + str(pnode.port))

Wyświetl plik

@ -0,0 +1,27 @@
# Generated by Django 2.1.7 on 2019-05-20 12:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('nodeodm', '0006_auto_20190220_1842'),
]
operations = [
migrations.RemoveField(
model_name='processingnode',
name='odm_version',
),
migrations.AddField(
model_name='processingnode',
name='engine',
field=models.CharField(help_text='Engine used by the node.', max_length=255, null=True),
),
migrations.AddField(
model_name='processingnode',
name='engine_version',
field=models.CharField(help_text='Engine version used by the node.', max_length=32, null=True),
),
]

Wyświetl plik

@ -25,8 +25,9 @@ class ProcessingNode(models.Model):
available_options = fields.JSONField(default=dict, help_text="Description of the options that can be used for processing") available_options = fields.JSONField(default=dict, help_text="Description of the options that can be used for processing")
token = models.CharField(max_length=1024, blank=True, default="", help_text="Token to use for authentication. If the node doesn't have authentication, you can leave this field blank.") token = models.CharField(max_length=1024, blank=True, default="", help_text="Token to use for authentication. If the node doesn't have authentication, you can leave this field blank.")
max_images = models.PositiveIntegerField(help_text="Maximum number of images accepted by this node.", blank=True, null=True) max_images = models.PositiveIntegerField(help_text="Maximum number of images accepted by this node.", blank=True, null=True)
odm_version = models.CharField(max_length=32, null=True, help_text="ODM version used by the node.") engine_version = models.CharField(max_length=32, null=True, help_text="Engine version used by the node.")
label = models.CharField(max_length=255, default="", blank=True, help_text="Optional label for this node. When set, this label will be shown instead of the hostname:port name.") label = models.CharField(max_length=255, default="", blank=True, help_text="Optional label for this node. When set, this label will be shown instead of the hostname:port name.")
engine = models.CharField(max_length=255, null=True, help_text="Engine used by the node.")
def __str__(self): def __str__(self):
if self.label != "": if self.label != "":
@ -61,7 +62,8 @@ class ProcessingNode(models.Model):
self.api_version = info.version self.api_version = info.version
self.queue_count = info.task_queue_count self.queue_count = info.task_queue_count
self.max_images = info.max_images self.max_images = info.max_images
self.odm_version = info.odm_version self.engine_version = info.engine_version
self.engine = info.engine
options = list(map(lambda o: o.__dict__, api_client.options())) options = list(map(lambda o: o.__dict__, api_client.options()))
self.available_options = options self.available_options = options
@ -116,7 +118,7 @@ class ProcessingNode(models.Model):
task = api_client.create_task(images, opts, name, progress_callback) task = api_client.create_task(images, opts, name, progress_callback)
return task.uuid return task.uuid
def get_task_info(self, uuid): def get_task_info(self, uuid, with_output=None):
""" """
Gets information about this task, such as name, creation date, Gets information about this task, such as name, creation date,
processing time, status, command line options and number of processing time, status, command line options and number of
@ -124,7 +126,13 @@ class ProcessingNode(models.Model):
""" """
api_client = self.api_client() api_client = self.api_client()
task = api_client.get_task(uuid) task = api_client.get_task(uuid)
return task.info() task_info = task.info(with_output)
# Output support for older clients
if not api_client.version_greater_or_equal_than("1.5.1") and with_output:
task_info.output = self.get_task_console_output(uuid, with_output)
return task_info
def get_task_console_output(self, uuid, line): def get_task_console_output(self, uuid, line):
""" """

Wyświetl plik

@ -37,7 +37,7 @@ pip-autoremove==0.9.0
psycopg2==2.7.4 psycopg2==2.7.4
psycopg2-binary==2.7.4 psycopg2-binary==2.7.4
PyJWT==1.5.3 PyJWT==1.5.3
pyodm==1.4.0 pyodm==1.5.1
pyparsing==2.1.10 pyparsing==2.1.10
pytz==2018.3 pytz==2018.3
rcssmin==1.0.6 rcssmin==1.0.6

Wyświetl plik

@ -9,6 +9,8 @@
"hostname": "nodeodm.masseranolabs.com", "hostname": "nodeodm.masseranolabs.com",
"port": 80, "port": 80,
"api_version": "1.0.1", "api_version": "1.0.1",
"engine_version": "0.6.0",
"engine": "odm",
"last_refreshed": "2017-03-01T21:14:49.918276Z", "last_refreshed": "2017-03-01T21:14:49.918276Z",
"queue_count": 0, "queue_count": 0,
"max_images": null, "max_images": null,
@ -34,7 +36,8 @@ online | bool | Whether the processing node could be reached in the last 5 minut
hostname | string | Hostname/IP address hostname | string | Hostname/IP address
port | int | Port port | int | Port
api_version | string | Version of NodeODM currently running api_version | string | Version of NodeODM currently running
odm_version | string | Version of ODM currently being used engine_version | string | Version of processing engine currently being used
engine | string | Lowercase identifier of processing engine
last_refreshed | string | Date and time this node was last seen online. This value is typically refreshed every 15-30 seconds and is used to decide whether a node is offline or not last_refreshed | string | Date and time this node was last seen online. This value is typically refreshed every 15-30 seconds and is used to decide whether a node is offline or not
queue_count | int | Number of [Task](#task) items currently being processed/queued on this node. queue_count | int | Number of [Task](#task) items currently being processed/queued on this node.
max_images | int | Optional maximum number of images this processing node can accept. null indicates no limit. max_images | int | Optional maximum number of images this processing node can accept. null indicates no limit.
@ -89,6 +92,8 @@ port | | "" | Filter by port
api_version | | "" | Filter by API version api_version | | "" | Filter by API version
queue_count | | "" | Filter by queue count queue_count | | "" | Filter by queue count
max_images | | "" | Filter by max images max_images | | "" | Filter by max images
engine_version | | "" | Filter by engine version
engine | | "" | Filter by engine identifier
ordering | | "" | Ordering field to sort results by ordering | | "" | Ordering field to sort results by
has_available_options | | "" | Return only processing nodes that have a valid set of processing options (check that the `available_options` field is populated). Either `true` or `false`. has_available_options | | "" | Return only processing nodes that have a valid set of processing options (check that the `available_options` field is populated). Either `true` or `false`.