mirror of
https://github.com/openharmony/third_party_sane-airscan.git
synced 2026-06-30 21:17:55 -04:00
44a8743825
Under certain circumstances, when the Kyocera ECOSYS M2040dn is connected via ipp-usb, it can take up to 12 seconds to respond to the eSCL/ScannerStatus after a successful scan. If we allow the scanner this additional time, it continues to operate correctly. However, with the default timeouts, this delay can cause scanning to fail. This issue is difficult to reproduce; it only occurs with XSane when scanning from the flatbed. It may also affect other scanners, but so far, no one has reported a similar issue. Everything else appears to be functioning correctly on both the sane-airscan and ipp-usb sides. I will continue to investigate this problem on the ipp-usb side. For now, I have increased the HTTP timeouts with some reasonable reserve for the "short" operations, such as requesting the scanner status and similar. This change is safe; it does not impact scanning when everything is functioning normally, but it may extend the time required for failure detection.
1902 lines
54 KiB
C
1902 lines
54 KiB
C
/* AirScan (a.k.a. eSCL) backend for SANE
|
|
*
|
|
* Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com)
|
|
* See LICENSE for license terms and conditions
|
|
*
|
|
* Device management
|
|
*/
|
|
|
|
#include "airscan.h"
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
|
|
/******************** Constants ********************/
|
|
/* HTTP timeouts, by operation, in milliseconds
|
|
*/
|
|
#define DEVICE_HTTP_TIMEOUT_DEVCAPS 20000
|
|
#define DEVICE_HTTP_TIMEOUT_PRECHECK 20000
|
|
#define DEVICE_HTTP_TIMEOUT_SCAN 30000
|
|
#define DEVICE_HTTP_TIMEOUT_LOAD -1
|
|
#define DEVICE_HTTP_TIMEOUT_CHECK 20000
|
|
#define DEVICE_HTTP_TIMEOUT_CLEANUP 30000
|
|
#define DEVICE_HTTP_TIMEOUT_CANCEL 30000
|
|
|
|
/* HTTP timeout for operation that was pending
|
|
* in a moment of cancel (if any)
|
|
*/
|
|
#define DEVICE_HTTP_TIMEOUT_CANCELED_OP 10000
|
|
|
|
/******************** Device management ********************/
|
|
/* Device flags
|
|
*/
|
|
enum {
|
|
DEVICE_SCANNING = (1 << 0), /* We are between sane_start() and
|
|
final sane_read() */
|
|
DEVICE_READING = (1 << 1) /* sane_read() can be called */
|
|
};
|
|
|
|
/* Device state diagram
|
|
*
|
|
* OPENED
|
|
* |
|
|
* V
|
|
* PROBING->PROBING_FAILED--------------------------------
|
|
* | |
|
|
* V |
|
|
* -->IDLE |
|
|
* | | submit PROTO_OP_SCAN |
|
|
* | V |
|
|
* | SCANNING---------- |
|
|
* | | | async cancel request received, |
|
|
* | | | dev->stm_cancel_event signalled |
|
|
* | | V |
|
|
* | | CANCEL_REQ |
|
|
* | | | dev->stm_cancel_event callback |
|
|
* | | | |
|
|
* | | +------ |
|
|
* | | reached | | PROTO_OP_SCAN still pending |
|
|
* | | CLEANUP | V |
|
|
* | |<----------------CANCEL_DELAYED |
|
|
* | | | | | PROTO_OP_SCAN failed |
|
|
* | | V V ----------------- |
|
|
* | | CANCEL_SENT | |
|
|
* | | job | | cancel request | |
|
|
* | | finished | | finished | |
|
|
* | | V V | |
|
|
* | | CANCEL_JOB_DONE CANCEL_REQ_DONE | |
|
|
* | | cancel req | | job | |
|
|
* | | finished | | finished | |
|
|
* | V | | | |
|
|
* | CLEANUP | | | |
|
|
* | | | | | |
|
|
* | V V V V |
|
|
* ---DONE<-------------------------------------- |
|
|
* | |
|
|
* V |
|
|
* CLOSED<------------------------------------------------
|
|
*/
|
|
typedef enum {
|
|
DEVICE_STM_OPENED,
|
|
DEVICE_STM_PROBING,
|
|
DEVICE_STM_PROBING_FAILED,
|
|
DEVICE_STM_IDLE,
|
|
DEVICE_STM_SCANNING,
|
|
DEVICE_STM_CANCEL_REQ,
|
|
DEVICE_STM_CANCEL_DELAYED,
|
|
DEVICE_STM_CANCEL_SENT,
|
|
DEVICE_STM_CANCEL_JOB_DONE,
|
|
DEVICE_STM_CANCEL_REQ_DONE,
|
|
DEVICE_STM_CLEANUP,
|
|
DEVICE_STM_DONE,
|
|
DEVICE_STM_CLOSED
|
|
} DEVICE_STM_STATE;
|
|
|
|
/* Device descriptor
|
|
*/
|
|
struct device {
|
|
/* Common part */
|
|
zeroconf_devinfo *devinfo; /* Device info */
|
|
log_ctx *log; /* Logging context */
|
|
unsigned int flags; /* Device flags */
|
|
devopt opt; /* Device options */
|
|
int checking_http_status; /* HTTP status before CHECK_STATUS */
|
|
|
|
/* State machinery */
|
|
DEVICE_STM_STATE stm_state; /* Device state */
|
|
pthread_cond_t stm_cond; /* Signalled when state changes */
|
|
eloop_event *stm_cancel_event; /* Signalled to initiate cancel */
|
|
http_query *stm_cancel_query; /* CANCEL query */
|
|
bool stm_cancel_sent; /* Cancel was sent to device */
|
|
eloop_timer *stm_timer; /* Delay timer */
|
|
struct timespec stm_last_fail_time;/* Last failed sane_start() time */
|
|
|
|
/* Protocol handling */
|
|
proto_ctx proto_ctx; /* Protocol handler context */
|
|
|
|
/* I/O handling (AVAHI and HTTP) */
|
|
zeroconf_endpoint *endpoint_current; /* Current endpoint to probe */
|
|
|
|
/* Job status */
|
|
SANE_Status job_status; /* Job completion status */
|
|
SANE_Word job_skip_x; /* How much pixels to skip, */
|
|
SANE_Word job_skip_y; /* from left and top */
|
|
|
|
/* Image decoders */
|
|
image_decoder *decoders[NUM_ID_FORMAT]; /* Decoders by format */
|
|
|
|
/* Read machinery */
|
|
SANE_Bool read_non_blocking; /* Non-blocking I/O mode */
|
|
pollable *read_pollable; /* Signalled when read won't
|
|
block */
|
|
http_data_queue *read_queue; /* Queue of received images */
|
|
http_data *read_image; /* Current image */
|
|
SANE_Byte *read_line_buf; /* Single-line buffer */
|
|
SANE_Int read_line_num; /* Current image line 0-based */
|
|
SANE_Int read_line_end; /* If read_line_num>read_line_end
|
|
no more lines left in image */
|
|
SANE_Int read_line_real_wid; /* Real line width */
|
|
SANE_Int read_line_off; /* Current offset in the line */
|
|
SANE_Int read_skip_bytes; /* How many bytes to skip at line
|
|
beginning */
|
|
bool read_24_to_8; /* Resample 24 to 8 bits */
|
|
filter *read_filters; /* Chain of image filters */
|
|
};
|
|
|
|
/* Static variables
|
|
*/
|
|
static device **device_table;
|
|
|
|
/* Forward declarations
|
|
*/
|
|
static device*
|
|
device_find_by_ident (const char *ident);
|
|
|
|
static void
|
|
device_http_cancel (device *dev);
|
|
|
|
static void
|
|
device_http_onerror (void *ptr, error err);
|
|
|
|
static void
|
|
device_proto_set (device *dev, ID_PROTO proto);
|
|
|
|
static void
|
|
device_scanner_capabilities_callback (void *ptr, http_query *q);
|
|
|
|
static void
|
|
device_probe_endpoint (device *dev, zeroconf_endpoint *endpoint);
|
|
|
|
static void
|
|
device_job_set_status (device *dev, SANE_Status status);
|
|
|
|
static inline DEVICE_STM_STATE
|
|
device_stm_state_get (device *dev);
|
|
|
|
static void
|
|
device_stm_state_set (device *dev, DEVICE_STM_STATE state);
|
|
|
|
static bool
|
|
device_stm_cancel_perform (device *dev, SANE_Status status);
|
|
|
|
static void
|
|
device_stm_op_callback (void *ptr, http_query *q);
|
|
|
|
static void
|
|
device_stm_cancel_event_callback (void *data);
|
|
|
|
static void
|
|
device_read_filters_setup (device *dev);
|
|
|
|
static void
|
|
device_read_filters_cleanup (device *dev);
|
|
|
|
static void
|
|
device_management_start_stop (bool start);
|
|
|
|
/******************** Device table management ********************/
|
|
/* Create a device.
|
|
*
|
|
* May fail. At this case, NULL will be returned and status will be set
|
|
*/
|
|
static device*
|
|
device_new (zeroconf_devinfo *devinfo)
|
|
{
|
|
device *dev;
|
|
int i;
|
|
|
|
/* Create device */
|
|
dev = mem_new(device, 1);
|
|
|
|
dev->devinfo = devinfo;
|
|
dev->log = log_ctx_new(dev->devinfo->name, NULL);
|
|
|
|
log_debug(dev->log, "device created");
|
|
|
|
dev->proto_ctx.log = dev->log;
|
|
dev->proto_ctx.devinfo = dev->devinfo;
|
|
dev->proto_ctx.devcaps = &dev->opt.caps;
|
|
|
|
devopt_init(&dev->opt);
|
|
|
|
dev->proto_ctx.http = http_client_new(dev->log, dev);
|
|
|
|
pthread_cond_init(&dev->stm_cond, NULL);
|
|
|
|
dev->read_pollable = pollable_new();
|
|
dev->read_queue = http_data_queue_new();
|
|
|
|
/* Setup decoders
|
|
*
|
|
* Note, unfortunately, some devices lies in their supported
|
|
* image formats list and may return image in different format,
|
|
* that requested. See, for example, #281 for details
|
|
*
|
|
* So we must instantiate all supported decoders and dynamically
|
|
* choose between them, based on the actual image content
|
|
*
|
|
* The previous approach, when we instantiated only decoders
|
|
* that we are about to use, doesn't work anymore
|
|
*/
|
|
image_decoder_create_all(dev->decoders);
|
|
for (i = 0; i < NUM_ID_FORMAT; i ++) {
|
|
if (dev->decoders[i] != NULL) {
|
|
log_debug(dev->log, "added image decoder: \"%s\"",
|
|
id_format_short_name(i));
|
|
}
|
|
}
|
|
|
|
/* Add to the table */
|
|
device_table = ptr_array_append(device_table, dev);
|
|
|
|
return dev;
|
|
}
|
|
|
|
/* Destroy a device
|
|
*/
|
|
static void
|
|
device_free (device *dev, const char *log_msg)
|
|
{
|
|
/* Remove device from table */
|
|
log_debug(dev->log, "removed from device table");
|
|
ptr_array_del(device_table, ptr_array_find(device_table, dev));
|
|
|
|
/* Stop all pending I/O activity */
|
|
device_http_cancel(dev);
|
|
|
|
if (dev->stm_cancel_event != NULL) {
|
|
eloop_event_free(dev->stm_cancel_event);
|
|
}
|
|
|
|
if (dev->stm_timer != NULL) {
|
|
eloop_timer_cancel(dev->stm_timer);
|
|
}
|
|
|
|
/* Release all memory */
|
|
device_proto_set(dev, ID_PROTO_UNKNOWN);
|
|
|
|
devopt_cleanup(&dev->opt);
|
|
|
|
http_client_free(dev->proto_ctx.http);
|
|
http_uri_free(dev->proto_ctx.base_uri);
|
|
http_uri_free(dev->proto_ctx.base_uri_nozone);
|
|
mem_free((char*) dev->proto_ctx.location);
|
|
|
|
pthread_cond_destroy(&dev->stm_cond);
|
|
image_decoder_free_all(dev->decoders);
|
|
http_data_queue_free(dev->read_queue);
|
|
pollable_free(dev->read_pollable);
|
|
device_read_filters_cleanup(dev);
|
|
|
|
log_debug(dev->log, "device destroyed");
|
|
if (log_msg != NULL) {
|
|
log_debug(dev->log, "%s", log_msg);
|
|
}
|
|
|
|
log_ctx_free(dev->log);
|
|
zeroconf_devinfo_free(dev->devinfo);
|
|
mem_free(dev);
|
|
}
|
|
|
|
/* Start probing. Called via eloop_call
|
|
*/
|
|
static void
|
|
device_start_probing (void *data)
|
|
{
|
|
device *dev = data;
|
|
|
|
device_probe_endpoint(dev, dev->devinfo->endpoints);
|
|
}
|
|
|
|
/* Start device I/O.
|
|
*/
|
|
static SANE_Status
|
|
device_io_start (device *dev)
|
|
{
|
|
dev->stm_cancel_event = eloop_event_new(device_stm_cancel_event_callback, dev);
|
|
if (dev->stm_cancel_event == NULL) {
|
|
return SANE_STATUS_NO_MEM;
|
|
}
|
|
|
|
device_stm_state_set(dev, DEVICE_STM_PROBING);
|
|
eloop_call(device_start_probing, dev);
|
|
|
|
return SANE_STATUS_GOOD;
|
|
}
|
|
|
|
/* Find device by ident
|
|
*/
|
|
static device*
|
|
device_find_by_ident (const char *ident)
|
|
{
|
|
size_t i, len = mem_len(device_table);
|
|
|
|
for (i = 0; i < len; i ++) {
|
|
device *dev = device_table[i];
|
|
if (!strcmp(dev->devinfo->ident, ident)) {
|
|
return dev;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* Purge device_table
|
|
*/
|
|
static void
|
|
device_table_purge (void)
|
|
{
|
|
while (mem_len(device_table) > 0) {
|
|
device_free(device_table[0], NULL);
|
|
}
|
|
}
|
|
|
|
/******************** Underlying protocol operations ********************/
|
|
/* Set protocol handler
|
|
*/
|
|
static void
|
|
device_proto_set (device *dev, ID_PROTO proto)
|
|
{
|
|
if (dev->proto_ctx.proto != NULL) {
|
|
log_debug(dev->log, "closed protocol \"%s\"",
|
|
dev->proto_ctx.proto->name);
|
|
dev->proto_ctx.proto->free(dev->proto_ctx.proto);
|
|
dev->proto_ctx.proto = NULL;
|
|
}
|
|
|
|
if (proto != ID_PROTO_UNKNOWN) {
|
|
dev->proto_ctx.proto = proto_handler_new(proto);
|
|
log_assert(dev->log, dev->proto_ctx.proto != NULL);
|
|
log_debug(dev->log, "using protocol \"%s\"",
|
|
dev->proto_ctx.proto->name);
|
|
}
|
|
}
|
|
|
|
/* Set base URI. `uri' ownership is taken by this function
|
|
*/
|
|
static void
|
|
device_proto_set_base_uri (device *dev, http_uri *uri)
|
|
{
|
|
http_uri_free(dev->proto_ctx.base_uri);
|
|
dev->proto_ctx.base_uri = uri;
|
|
|
|
http_uri_free(dev->proto_ctx.base_uri_nozone);
|
|
dev->proto_ctx.base_uri_nozone = http_uri_clone(uri);
|
|
http_uri_strip_zone_suffux(dev->proto_ctx.base_uri_nozone);
|
|
}
|
|
|
|
/* Query device capabilities
|
|
*/
|
|
static void
|
|
device_proto_devcaps_submit (device *dev, void (*callback) (void*, http_query*))
|
|
{
|
|
http_query *q;
|
|
|
|
q = dev->proto_ctx.proto->devcaps_query(&dev->proto_ctx);
|
|
http_query_timeout(q, DEVICE_HTTP_TIMEOUT_DEVCAPS);
|
|
http_query_submit(q, callback);
|
|
dev->proto_ctx.query = q;
|
|
}
|
|
|
|
/* Decode device capabilities
|
|
*/
|
|
static error
|
|
device_proto_devcaps_decode (device *dev, devcaps *caps)
|
|
{
|
|
return dev->proto_ctx.proto->devcaps_decode(&dev->proto_ctx, caps);
|
|
}
|
|
|
|
/* http_query_onrxhdr() callback
|
|
*/
|
|
static void
|
|
device_proto_op_onrxhdr (void *p, http_query *q)
|
|
{
|
|
device *dev = p;
|
|
|
|
if (dev->proto_ctx.op == PROTO_OP_LOAD && !dev->stm_cancel_sent) {
|
|
http_query_timeout(q, -1);
|
|
}
|
|
}
|
|
|
|
/* Submit operation request
|
|
*/
|
|
static void
|
|
device_proto_op_submit (device *dev, PROTO_OP op,
|
|
void (*callback) (void*, http_query*))
|
|
{
|
|
http_query *(*func) (const proto_ctx *ctx) = NULL;
|
|
int timeout = -1;
|
|
http_query *q;
|
|
|
|
switch (op) {
|
|
case PROTO_OP_NONE: log_internal_error(dev->log); break;
|
|
case PROTO_OP_FINISH: log_internal_error(dev->log); break;
|
|
|
|
case PROTO_OP_PRECHECK:
|
|
func = dev->proto_ctx.proto->precheck_query;
|
|
timeout = DEVICE_HTTP_TIMEOUT_PRECHECK;
|
|
break;
|
|
|
|
case PROTO_OP_SCAN:
|
|
func = dev->proto_ctx.proto->scan_query;
|
|
timeout = DEVICE_HTTP_TIMEOUT_SCAN;
|
|
break;
|
|
|
|
case PROTO_OP_LOAD:
|
|
func = dev->proto_ctx.proto->load_query;
|
|
timeout = DEVICE_HTTP_TIMEOUT_LOAD;
|
|
break;
|
|
|
|
case PROTO_OP_CHECK:
|
|
func = dev->proto_ctx.proto->status_query;
|
|
timeout = DEVICE_HTTP_TIMEOUT_CHECK;
|
|
break;
|
|
|
|
case PROTO_OP_CLEANUP:
|
|
func = dev->proto_ctx.proto->cleanup_query;
|
|
timeout = DEVICE_HTTP_TIMEOUT_CLEANUP;
|
|
break;
|
|
}
|
|
|
|
log_assert(dev->log, func != NULL);
|
|
|
|
log_debug(dev->log, "%s: submitting: attempt=%d",
|
|
proto_op_name(op), dev->proto_ctx.failed_attempt);
|
|
dev->proto_ctx.op = op;
|
|
|
|
q = func(&dev->proto_ctx);
|
|
http_query_timeout(q, timeout);
|
|
if (op == PROTO_OP_LOAD) {
|
|
http_query_onrxhdr(q, device_proto_op_onrxhdr);
|
|
}
|
|
|
|
http_query_submit(q, callback);
|
|
dev->proto_ctx.query = q;
|
|
}
|
|
|
|
/* Dummy decode for PROTO_OP_CANCEL and PROTO_OP_CLEANUP
|
|
*/
|
|
static proto_result
|
|
device_proto_dummy_decode (const proto_ctx *ctx)
|
|
{
|
|
proto_result result = {0};
|
|
|
|
(void) ctx;
|
|
result.next = PROTO_OP_FINISH;
|
|
|
|
return result;
|
|
}
|
|
|
|
/* Decode operation response
|
|
*/
|
|
static proto_result
|
|
device_proto_op_decode (device *dev, PROTO_OP op)
|
|
{
|
|
proto_result (*func) (const proto_ctx *ctx) = NULL;
|
|
proto_result result;
|
|
|
|
switch (op) {
|
|
case PROTO_OP_NONE: log_internal_error(dev->log); break;
|
|
case PROTO_OP_PRECHECK:func = dev->proto_ctx.proto->precheck_decode; break;
|
|
case PROTO_OP_SCAN: func = dev->proto_ctx.proto->scan_decode; break;
|
|
case PROTO_OP_LOAD: func = dev->proto_ctx.proto->load_decode; break;
|
|
case PROTO_OP_CHECK: func = dev->proto_ctx.proto->status_decode; break;
|
|
case PROTO_OP_CLEANUP: func = device_proto_dummy_decode; break;
|
|
case PROTO_OP_FINISH: log_internal_error(dev->log); break;
|
|
}
|
|
|
|
log_assert(dev->log, func != NULL);
|
|
|
|
log_debug(dev->log, "%s: decoding", proto_op_name(op));
|
|
result = func(&dev->proto_ctx);
|
|
log_debug(dev->log, "%s: decoded: status=\"%s\" next=%s delay=%d",
|
|
proto_op_name(op),
|
|
sane_strstatus(result.status),
|
|
proto_op_name(result.next),
|
|
result.delay);
|
|
|
|
if (result.next == PROTO_OP_CHECK) {
|
|
int http_status = http_query_status(dev->proto_ctx.query);
|
|
|
|
dev->proto_ctx.failed_op = op;
|
|
dev->proto_ctx.failed_http_status = http_status;
|
|
}
|
|
|
|
if (op == PROTO_OP_CHECK) {
|
|
dev->proto_ctx.failed_attempt ++;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/******************** HTTP operations ********************/
|
|
/* Cancel pending HTTP request, if any
|
|
*/
|
|
static void
|
|
device_http_cancel (device *dev)
|
|
{
|
|
http_client_cancel(dev->proto_ctx.http);
|
|
|
|
if (dev->stm_timer != NULL) {
|
|
eloop_timer_cancel(dev->stm_timer);
|
|
dev->stm_timer = NULL;
|
|
}
|
|
}
|
|
|
|
/* http_client onerror callback
|
|
*/
|
|
static void
|
|
device_http_onerror (void *ptr, error err)
|
|
{
|
|
device *dev = ptr;
|
|
SANE_Status status;
|
|
|
|
status = err == ERROR_ENOMEM ? SANE_STATUS_NO_MEM : SANE_STATUS_IO_ERROR;
|
|
|
|
log_debug(dev->log, "cancelling job due to error: %s", ESTRING(err));
|
|
|
|
if (!device_stm_cancel_perform(dev, status)) {
|
|
device_stm_state_set(dev, DEVICE_STM_DONE);
|
|
} else {
|
|
/* Scan job known to be done, now waiting for cancel
|
|
* completion
|
|
*/
|
|
device_stm_state_set(dev, DEVICE_STM_CANCEL_JOB_DONE);
|
|
}
|
|
}
|
|
|
|
/******************** Protocol initialization ********************/
|
|
/* Probe next device address
|
|
*/
|
|
static void
|
|
device_probe_endpoint (device *dev, zeroconf_endpoint *endpoint)
|
|
{
|
|
/* Switch endpoint */
|
|
log_assert(dev->log, endpoint->proto != ID_PROTO_UNKNOWN);
|
|
|
|
if (dev->endpoint_current == NULL ||
|
|
dev->endpoint_current->proto != endpoint->proto) {
|
|
device_proto_set(dev, endpoint->proto);
|
|
}
|
|
|
|
dev->endpoint_current = endpoint;
|
|
|
|
device_proto_set_base_uri(dev, http_uri_clone(endpoint->uri));
|
|
|
|
/* Fetch device capabilities */
|
|
device_proto_devcaps_submit (dev, device_scanner_capabilities_callback);
|
|
}
|
|
|
|
/* Scanner capabilities fetch callback
|
|
*/
|
|
static void
|
|
device_scanner_capabilities_callback (void *ptr, http_query *q)
|
|
{
|
|
error err = NULL;
|
|
device *dev = ptr;
|
|
|
|
/* Check request status */
|
|
err = http_query_error(q);
|
|
if (err != NULL) {
|
|
err = eloop_eprintf("scanner capabilities query: %s", ESTRING(err));
|
|
goto DONE;
|
|
}
|
|
|
|
/* Parse XML response */
|
|
err = device_proto_devcaps_decode (dev, &dev->opt.caps);
|
|
if (err != NULL) {
|
|
err = eloop_eprintf("scanner capabilities: %s", err);
|
|
goto DONE;
|
|
}
|
|
|
|
devcaps_dump(dev->log, &dev->opt.caps, true);
|
|
devopt_set_defaults(&dev->opt);
|
|
|
|
/* Update endpoint address in case of HTTP redirection */
|
|
if (!http_uri_equal(http_query_uri(q), http_query_real_uri(q))) {
|
|
const char *uri_str = http_uri_str(http_query_uri(q));
|
|
const char *real_uri_str = http_uri_str(http_query_real_uri(q));
|
|
const char *base_str = http_uri_str(dev->proto_ctx.base_uri);
|
|
|
|
if (str_has_prefix(uri_str, base_str)) {
|
|
const char *tail = uri_str + strlen(base_str);
|
|
|
|
if (str_has_suffix(real_uri_str, tail)) {
|
|
size_t l = strlen(real_uri_str) - strlen(tail);
|
|
char *new_uri_str = alloca(l + 1);
|
|
http_uri *new_uri;
|
|
|
|
memcpy(new_uri_str, real_uri_str, l);
|
|
new_uri_str[l] = '\0';
|
|
|
|
log_debug(dev->log, "endpoint URI changed due to redirection:");
|
|
log_debug(dev->log, " old URL: %s", base_str);
|
|
log_debug(dev->log, " new URL: %s", new_uri_str);
|
|
|
|
new_uri = http_uri_new(new_uri_str, true);
|
|
log_assert(dev->log, new_uri != NULL);
|
|
|
|
device_proto_set_base_uri(dev, new_uri);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Cleanup and exit */
|
|
DONE:
|
|
if (err != NULL) {
|
|
log_debug(dev->log, ESTRING(err));
|
|
|
|
if (dev->endpoint_current != NULL &&
|
|
dev->endpoint_current->next != NULL) {
|
|
device_probe_endpoint(dev, dev->endpoint_current->next);
|
|
} else {
|
|
device_stm_state_set(dev, DEVICE_STM_PROBING_FAILED);
|
|
}
|
|
} else {
|
|
device_stm_state_set(dev, DEVICE_STM_IDLE);
|
|
http_client_onerror(dev->proto_ctx.http, device_http_onerror);
|
|
}
|
|
}
|
|
|
|
/******************** Scan state machinery ********************/
|
|
/* Get state name, for debugging
|
|
*/
|
|
static const char*
|
|
device_stm_state_name (DEVICE_STM_STATE state)
|
|
{
|
|
switch (state) {
|
|
case DEVICE_STM_OPENED: return "DEVICE_STM_OPENED";
|
|
case DEVICE_STM_PROBING: return "DEVICE_STM_PROBING";
|
|
case DEVICE_STM_PROBING_FAILED: return "DEVICE_STM_PROBING_FAILED";
|
|
case DEVICE_STM_IDLE: return "DEVICE_STM_IDLE";
|
|
case DEVICE_STM_SCANNING: return "DEVICE_STM_SCANNING";
|
|
case DEVICE_STM_CANCEL_REQ: return "DEVICE_STM_CANCEL_REQ";
|
|
case DEVICE_STM_CANCEL_DELAYED: return "DEVICE_STM_CANCEL_DELAYED";
|
|
case DEVICE_STM_CANCEL_SENT: return "DEVICE_STM_CANCEL_SENT";
|
|
case DEVICE_STM_CANCEL_JOB_DONE: return "DEVICE_STM_CANCEL_JOB_DONE";
|
|
case DEVICE_STM_CANCEL_REQ_DONE: return "DEVICE_STM_CANCEL_REQ_DONE";
|
|
case DEVICE_STM_CLEANUP: return "DEVICE_STM_CLEANUP";
|
|
case DEVICE_STM_DONE: return "DEVICE_STM_DONE";
|
|
case DEVICE_STM_CLOSED: return "DEVICE_STM_CLOSED";
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* Get state
|
|
*/
|
|
static inline DEVICE_STM_STATE
|
|
device_stm_state_get (device *dev)
|
|
{
|
|
return __atomic_load_n(&dev->stm_state, __ATOMIC_SEQ_CST);
|
|
}
|
|
|
|
/* Check if device is in working state
|
|
*/
|
|
static bool
|
|
device_stm_state_working (device *dev)
|
|
{
|
|
DEVICE_STM_STATE state = device_stm_state_get(dev);
|
|
return state > DEVICE_STM_IDLE && state < DEVICE_STM_DONE;
|
|
}
|
|
|
|
/* Set state
|
|
*/
|
|
static void
|
|
device_stm_state_set (device *dev, DEVICE_STM_STATE state)
|
|
{
|
|
DEVICE_STM_STATE old_state = device_stm_state_get(dev);
|
|
|
|
if (old_state != state) {
|
|
log_debug(dev->log, "%s->%s",
|
|
device_stm_state_name(old_state), device_stm_state_name(state));
|
|
|
|
__atomic_store_n(&dev->stm_state, state, __ATOMIC_SEQ_CST);
|
|
pthread_cond_broadcast(&dev->stm_cond);
|
|
|
|
if (!device_stm_state_working(dev)) {
|
|
pollable_signal(dev->read_pollable);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* cancel_query() callback
|
|
*/
|
|
static void
|
|
device_stm_cancel_callback (void *ptr, http_query *q)
|
|
{
|
|
device *dev = ptr;
|
|
|
|
(void) q;
|
|
|
|
dev->stm_cancel_query = NULL;
|
|
if (device_stm_state_get(dev) == DEVICE_STM_CANCEL_JOB_DONE) {
|
|
device_stm_state_set(dev, DEVICE_STM_DONE);
|
|
} else {
|
|
device_stm_state_set(dev, DEVICE_STM_CANCEL_REQ_DONE);
|
|
}
|
|
}
|
|
|
|
/* Perform cancel, if possible
|
|
*/
|
|
static bool
|
|
device_stm_cancel_perform (device *dev, SANE_Status status)
|
|
{
|
|
proto_ctx *ctx = &dev->proto_ctx;
|
|
|
|
device_job_set_status(dev, status);
|
|
if (ctx->location != NULL && !dev->stm_cancel_sent) {
|
|
if (ctx->params.src == ID_SOURCE_PLATEN &&
|
|
ctx->images_received > 0) {
|
|
/* If we are not expecting more images, skip cancel
|
|
* and simple wait until job is done
|
|
*
|
|
* Otherwise Xerox VersaLink B405 remains busy for
|
|
* a quite long time without any need
|
|
*/
|
|
log_debug(dev->log, "cancel skipped as job is almost done");
|
|
return false;
|
|
} else {
|
|
/* Otherwise, perform a normal cancel operation
|
|
*/
|
|
device_stm_state_set(dev, DEVICE_STM_CANCEL_SENT);
|
|
|
|
log_assert(dev->log, dev->stm_cancel_query == NULL);
|
|
dev->stm_cancel_query = ctx->proto->cancel_query(ctx);
|
|
|
|
http_query_onerror(dev->stm_cancel_query, NULL);
|
|
http_query_timeout(dev->stm_cancel_query,
|
|
DEVICE_HTTP_TIMEOUT_CANCEL);
|
|
|
|
http_client_timeout(dev->proto_ctx.http,
|
|
DEVICE_HTTP_TIMEOUT_CANCELED_OP);
|
|
|
|
http_query_submit(dev->stm_cancel_query, device_stm_cancel_callback);
|
|
|
|
dev->stm_cancel_sent = true;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/* stm_cancel_event callback
|
|
*/
|
|
static void
|
|
device_stm_cancel_event_callback (void *data)
|
|
{
|
|
device *dev = data;
|
|
|
|
log_debug(dev->log, "cancel processing started");
|
|
if (!device_stm_cancel_perform(dev, SANE_STATUS_CANCELLED)) {
|
|
device_stm_state_set(dev, DEVICE_STM_CANCEL_DELAYED);
|
|
}
|
|
}
|
|
|
|
/* Request cancel.
|
|
*
|
|
* Note, reason must be NULL, if cancel requested from the signal handler
|
|
*/
|
|
static void
|
|
device_stm_cancel_req (device *dev, const char *reason)
|
|
{
|
|
DEVICE_STM_STATE expected = DEVICE_STM_SCANNING;
|
|
bool ok = __atomic_compare_exchange_n(&dev->stm_state, &expected,
|
|
DEVICE_STM_CANCEL_REQ, true, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST);
|
|
|
|
if (ok) {
|
|
if (reason != NULL) {
|
|
log_debug(dev->log, "cancel requested: %s", reason);
|
|
}
|
|
|
|
eloop_event_trigger(dev->stm_cancel_event);
|
|
}
|
|
}
|
|
|
|
/* stm_timer callback
|
|
*/
|
|
static void
|
|
device_stm_timer_callback (void *data)
|
|
{
|
|
device *dev = data;
|
|
dev->stm_timer = NULL;
|
|
device_proto_op_submit(dev, dev->proto_ctx.op, device_stm_op_callback);
|
|
}
|
|
|
|
/* Operation callback
|
|
*/
|
|
static void
|
|
device_stm_op_callback (void *ptr, http_query *q)
|
|
{
|
|
device *dev = ptr;
|
|
proto_result result = device_proto_op_decode(dev, dev->proto_ctx.op);
|
|
|
|
(void) q;
|
|
|
|
if (result.err != NULL) {
|
|
log_debug(dev->log, "%s", ESTRING(result.err));
|
|
}
|
|
|
|
/* Save useful result, if any */
|
|
if (dev->proto_ctx.op == PROTO_OP_SCAN) {
|
|
if (result.data.location != NULL) {
|
|
mem_free((char*) dev->proto_ctx.location); /* Just in case */
|
|
dev->proto_ctx.location = result.data.location;
|
|
dev->proto_ctx.failed_attempt = 0;
|
|
pthread_cond_broadcast(&dev->stm_cond);
|
|
}
|
|
} else if (dev->proto_ctx.op == PROTO_OP_LOAD) {
|
|
if (result.data.image != NULL) {
|
|
http_data_queue_push(dev->read_queue, result.data.image);
|
|
dev->proto_ctx.images_received ++;
|
|
pollable_signal(dev->read_pollable);
|
|
|
|
dev->proto_ctx.failed_attempt = 0;
|
|
pthread_cond_broadcast(&dev->stm_cond);
|
|
}
|
|
}
|
|
|
|
/* Update job status */
|
|
device_job_set_status(dev, result.status);
|
|
|
|
/* If CANCEL was sent, and next operation is CLEANUP or
|
|
* current operation is CHECK, FINISH the job
|
|
*/
|
|
if (dev->stm_cancel_sent) {
|
|
if (result.next == PROTO_OP_CLEANUP ||
|
|
dev->proto_ctx.op == PROTO_OP_CHECK) {
|
|
result.next = PROTO_OP_FINISH;
|
|
}
|
|
}
|
|
|
|
/* Check for FINISH */
|
|
if (result.next == PROTO_OP_FINISH) {
|
|
if (dev->proto_ctx.images_received == 0) {
|
|
/* If no images received, and no error status
|
|
* yet set, use SANE_STATUS_IO_ERROR as default
|
|
* error code
|
|
*/
|
|
device_job_set_status(dev, SANE_STATUS_IO_ERROR);
|
|
}
|
|
|
|
if (device_stm_state_get(dev) == DEVICE_STM_CANCEL_SENT) {
|
|
device_stm_state_set(dev, DEVICE_STM_CANCEL_JOB_DONE);
|
|
} else {
|
|
device_stm_state_set(dev, DEVICE_STM_DONE);
|
|
}
|
|
return;
|
|
}
|
|
|
|
/* Handle switch to PROTO_OP_CLEANUP state */
|
|
if (result.next == PROTO_OP_CLEANUP) {
|
|
device_stm_state_set(dev, DEVICE_STM_CLEANUP);
|
|
}
|
|
|
|
/* Handle delayed cancellation */
|
|
if (device_stm_state_get(dev) == DEVICE_STM_CANCEL_DELAYED) {
|
|
if (!device_stm_cancel_perform(dev, SANE_STATUS_CANCELLED)) {
|
|
/* Finish the job, if we has not yet reached cancellable
|
|
* state
|
|
*/
|
|
device_stm_state_set(dev, DEVICE_STM_DONE);
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* Handle delay */
|
|
if (result.delay != 0) {
|
|
log_assert(dev->log, dev->stm_timer == NULL);
|
|
dev->stm_timer = eloop_timer_new(result.delay,
|
|
device_stm_timer_callback, dev);
|
|
dev->proto_ctx.op = result.next;
|
|
return;
|
|
}
|
|
|
|
/* Submit next operation */
|
|
device_proto_op_submit(dev, result.next, device_stm_op_callback);
|
|
}
|
|
|
|
/* Geometrical scan parameters
|
|
*/
|
|
typedef struct {
|
|
SANE_Word off; /* Requested X/Y offset, in pixels assuming 300 DPI */
|
|
SANE_Word len; /* Requested width/height, in pixels, assuming 300 DPI */
|
|
SANE_Word skip; /* Pixels to skip in returned image, in pixels assuming
|
|
actual resolution */
|
|
}
|
|
device_geom;
|
|
|
|
/*
|
|
* Computing geometrical scan parameters
|
|
*
|
|
* Input:
|
|
* tl, br - top-left, bottom-rights X/Y, in mm
|
|
* minlen, maxlen - device-defined min and max width or height,
|
|
* in pixels, assuming 300 dpi
|
|
* res - scan resolution
|
|
*
|
|
* Output:
|
|
* Filled device_geom structure
|
|
*
|
|
* Problem description.
|
|
*
|
|
* First of all, we use 3 different units to deal with geometrical
|
|
* parameters:
|
|
* 1) we communicate with frontend in millimeters
|
|
* 2) we communicate with scanner in pixels, assuming
|
|
* protocol-specific DPI (defined by devcaps::units)
|
|
* 3) when we deal with image, sizes are in pixels in real resolution
|
|
*
|
|
* Second, scanner returns minimal and maximal window size, but
|
|
* to simplify frontend's life, we pretend there is no such thing,
|
|
* as a minimal width or height, otherwise TL and BR ranges become
|
|
* dependent from each other. Instead, we always request image from
|
|
* scanner not smaller that scanner's minimum, and clip excessive
|
|
* image parts if required.
|
|
*
|
|
* This all makes things non-trivial. This function handles
|
|
* this complexity
|
|
*/
|
|
static device_geom
|
|
device_geom_compute (SANE_Fixed tl, SANE_Fixed br,
|
|
SANE_Word minlen, SANE_Word maxlen, SANE_Word res, SANE_Word units)
|
|
{
|
|
device_geom geom;
|
|
|
|
geom.off = math_mm2px_res(tl, units);
|
|
geom.len = math_mm2px_res(br - tl, units);
|
|
geom.skip = 0;
|
|
|
|
minlen = math_max(minlen, 1);
|
|
geom.len = math_bound(geom.len, minlen, maxlen);
|
|
|
|
if (geom.off + geom.len > maxlen) {
|
|
geom.skip = geom.off + geom.len - maxlen;
|
|
geom.off -= geom.skip;
|
|
|
|
geom.skip = math_muldiv(geom.skip, res, units);
|
|
}
|
|
|
|
return geom;
|
|
}
|
|
|
|
/* Choose image format
|
|
*/
|
|
static ID_FORMAT
|
|
device_choose_format (device *dev, devcaps_source *src)
|
|
{
|
|
unsigned int formats = src->formats & DEVCAPS_FORMATS_SUPPORTED;
|
|
size_t i;
|
|
static const ID_FORMAT use[] = {
|
|
ID_FORMAT_PNG,
|
|
ID_FORMAT_JPEG,
|
|
ID_FORMAT_TIFF,
|
|
ID_FORMAT_BMP
|
|
};
|
|
|
|
for (i = 0; i < sizeof(use)/sizeof(use[0]); i ++) {
|
|
ID_FORMAT fmt = use[i];
|
|
if ((formats & (1 << fmt)) != 0) {
|
|
return fmt;
|
|
}
|
|
}
|
|
|
|
log_internal_error(dev->log);
|
|
return ID_FORMAT_UNKNOWN;
|
|
}
|
|
|
|
/* Request scan
|
|
*/
|
|
static void
|
|
device_stm_start_scan (device *dev)
|
|
{
|
|
device_geom geom_x, geom_y;
|
|
proto_ctx *ctx = &dev->proto_ctx;
|
|
proto_scan_params *params = &ctx->params;
|
|
devcaps_source *src = dev->opt.caps.src[dev->opt.src];
|
|
SANE_Word x_resolution = dev->opt.resolution;
|
|
SANE_Word y_resolution = dev->opt.resolution;
|
|
char buf[64];
|
|
|
|
/* Prepare window parameters */
|
|
geom_x = device_geom_compute(dev->opt.tl_x, dev->opt.br_x,
|
|
src->min_wid_px, src->max_wid_px, x_resolution, dev->opt.caps.units);
|
|
|
|
geom_y = device_geom_compute(dev->opt.tl_y, dev->opt.br_y,
|
|
src->min_hei_px, src->max_hei_px, y_resolution, dev->opt.caps.units);
|
|
|
|
dev->job_skip_x = geom_x.skip;
|
|
dev->job_skip_y = geom_y.skip;
|
|
|
|
/* Fill proto_scan_params structure */
|
|
memset(params, 0, sizeof(*params));
|
|
params->x_off = geom_x.off;
|
|
params->y_off = geom_y.off;
|
|
params->wid = geom_x.len;
|
|
params->hei = geom_y.len;
|
|
params->x_res = x_resolution;
|
|
params->y_res = y_resolution;
|
|
params->src = dev->opt.src;
|
|
params->colormode = dev->opt.colormode_real;
|
|
params->scanintent = dev->opt.scanintent;
|
|
params->format = device_choose_format(dev, src);
|
|
|
|
/* Dump parameters */
|
|
log_trace(dev->log, "==============================");
|
|
log_trace(dev->log, "Starting scan, using the following parameters:");
|
|
log_trace(dev->log, " source: %s",
|
|
id_source_sane_name(params->src));
|
|
log_trace(dev->log, " colormode_emul: %s",
|
|
id_colormode_sane_name(dev->opt.colormode_emul));
|
|
log_trace(dev->log, " colormode_real: %s",
|
|
id_colormode_sane_name(params->colormode));
|
|
log_trace(dev->log, " scanintent: %s",
|
|
id_scanintent_sane_name(params->scanintent));
|
|
log_trace(dev->log, " tl_x: %s mm",
|
|
math_fmt_mm(dev->opt.tl_x, buf));
|
|
log_trace(dev->log, " tl_y: %s mm",
|
|
math_fmt_mm(dev->opt.tl_y, buf));
|
|
log_trace(dev->log, " br_x: %s mm",
|
|
math_fmt_mm(dev->opt.br_x, buf));
|
|
log_trace(dev->log, " br_y: %s mm",
|
|
math_fmt_mm(dev->opt.br_y, buf));
|
|
log_trace(dev->log, " image size: %dx%d", params->wid, params->hei);
|
|
log_trace(dev->log, " image X offset: %d", params->x_off);
|
|
log_trace(dev->log, " image Y offset: %d", params->y_off);
|
|
log_trace(dev->log, " x_resolution: %d", params->x_res);
|
|
log_trace(dev->log, " y_resolution: %d", params->y_res);
|
|
log_trace(dev->log, " format: %s",
|
|
id_format_short_name(params->format));
|
|
log_trace(dev->log, "");
|
|
|
|
/* Submit a request */
|
|
device_stm_state_set(dev, DEVICE_STM_SCANNING);
|
|
if (dev->proto_ctx.proto->precheck_query != NULL) {
|
|
device_proto_op_submit(dev, PROTO_OP_PRECHECK, device_stm_op_callback);
|
|
} else {
|
|
device_proto_op_submit(dev, PROTO_OP_SCAN, device_stm_op_callback);
|
|
}
|
|
}
|
|
|
|
/* Wait until device leaves the working state
|
|
*/
|
|
static void
|
|
device_stm_wait_while_working (device *dev)
|
|
{
|
|
while (device_stm_state_working(dev)) {
|
|
eloop_cond_wait(&dev->stm_cond);
|
|
}
|
|
}
|
|
|
|
/* Cancel scanning and wait until device leaves the working state
|
|
*/
|
|
static void
|
|
device_stm_cancel_wait (device *dev, const char *reason)
|
|
{
|
|
device_stm_cancel_req(dev, reason);
|
|
device_stm_wait_while_working(dev);
|
|
}
|
|
|
|
/******************** Scan Job management ********************/
|
|
/* Set job status. If status already set, it will not be changed
|
|
*/
|
|
static void
|
|
device_job_set_status (device *dev, SANE_Status status)
|
|
{
|
|
/* Check status, new and present
|
|
*/
|
|
switch (status) {
|
|
case SANE_STATUS_GOOD:
|
|
return;
|
|
|
|
case SANE_STATUS_CANCELLED:
|
|
break;
|
|
|
|
default:
|
|
/* If error already is pending, leave it as is
|
|
*/
|
|
if (dev->job_status != SANE_STATUS_GOOD) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* Update status
|
|
*/
|
|
if (status != dev->job_status) {
|
|
log_debug(dev->log, "JOB status=%s", sane_strstatus(status));
|
|
dev->job_status = status;
|
|
|
|
if (status == SANE_STATUS_CANCELLED) {
|
|
http_data_queue_purge(dev->read_queue);
|
|
}
|
|
}
|
|
}
|
|
|
|
/******************** API helpers ********************/
|
|
/* Get device's logging context
|
|
*/
|
|
log_ctx*
|
|
device_log_ctx (device *dev)
|
|
{
|
|
return dev ? dev->log : NULL;
|
|
}
|
|
|
|
/* Open a device
|
|
*/
|
|
device*
|
|
device_open (const char *ident, SANE_Status *status)
|
|
{
|
|
device *dev = NULL;
|
|
zeroconf_devinfo *devinfo;
|
|
|
|
*status = SANE_STATUS_GOOD;
|
|
|
|
/* Validate arguments */
|
|
if (ident == NULL || *ident == '\0') {
|
|
log_debug(NULL, "device_open: invalid name");
|
|
*status = SANE_STATUS_INVAL;
|
|
return NULL;
|
|
}
|
|
|
|
/* Already opened? */
|
|
dev = device_find_by_ident(ident);
|
|
if (dev) {
|
|
*status = SANE_STATUS_DEVICE_BUSY;
|
|
return NULL;
|
|
}
|
|
|
|
/* Obtain device endpoints */
|
|
devinfo = zeroconf_devinfo_lookup(ident);
|
|
if (devinfo == NULL) {
|
|
log_debug(NULL, "device_open(%s): device not found", ident);
|
|
*status = SANE_STATUS_INVAL;
|
|
return NULL;
|
|
}
|
|
|
|
/* Create a device */
|
|
dev = device_new(devinfo);
|
|
*status = device_io_start(dev);
|
|
if (*status != SANE_STATUS_GOOD) {
|
|
device_free(dev, NULL);
|
|
return NULL;
|
|
}
|
|
|
|
/* Wait until device is initialized */
|
|
while (device_stm_state_get(dev) == DEVICE_STM_PROBING) {
|
|
eloop_cond_wait(&dev->stm_cond);
|
|
}
|
|
|
|
if (device_stm_state_get(dev) == DEVICE_STM_PROBING_FAILED) {
|
|
device_free(dev, NULL);
|
|
*status = SANE_STATUS_IO_ERROR;
|
|
return NULL;
|
|
}
|
|
|
|
return dev;
|
|
}
|
|
|
|
/* Close the device
|
|
* If log_msg is not NULL, it is written to the device log as late as possible
|
|
*/
|
|
void
|
|
device_close (device *dev, const char *log_msg)
|
|
{
|
|
/* Cancel job in progress, if any */
|
|
if (device_stm_state_working(dev)) {
|
|
device_stm_cancel_wait(dev, "device close");
|
|
}
|
|
|
|
/* Close the device */
|
|
device_stm_state_set(dev, DEVICE_STM_CLOSED);
|
|
device_free(dev, log_msg);
|
|
}
|
|
|
|
/* Get option descriptor
|
|
*/
|
|
const SANE_Option_Descriptor*
|
|
device_get_option_descriptor (device *dev, SANE_Int option)
|
|
{
|
|
if (0 <= option && option < NUM_OPTIONS) {
|
|
return &dev->opt.desc[option];
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* Get device option
|
|
*/
|
|
SANE_Status
|
|
device_get_option (device *dev, SANE_Int option, void *value)
|
|
{
|
|
return devopt_get_option(&dev->opt, option, value);
|
|
}
|
|
|
|
/* Set device option
|
|
*/
|
|
SANE_Status
|
|
device_set_option (device *dev, SANE_Int option, void *value, SANE_Word *info)
|
|
{
|
|
SANE_Status status;
|
|
|
|
if ((dev->flags & DEVICE_SCANNING) != 0) {
|
|
log_debug(dev->log, "device_set_option: already scanning");
|
|
return SANE_STATUS_INVAL;
|
|
}
|
|
|
|
status = devopt_set_option(&dev->opt, option, value, info);
|
|
if (status == SANE_STATUS_GOOD && opt_is_enhancement(option)) {
|
|
device_read_filters_setup(dev);
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
/* Get current scan parameters
|
|
*/
|
|
SANE_Status
|
|
device_get_parameters (device *dev, SANE_Parameters *params)
|
|
{
|
|
*params = dev->opt.params;
|
|
return SANE_STATUS_GOOD;
|
|
}
|
|
|
|
/* Start scanning operation - runs on a context of event loop thread
|
|
*/
|
|
static void
|
|
device_start_do (void *data)
|
|
{
|
|
device *dev = data;
|
|
|
|
device_stm_start_scan(dev);
|
|
}
|
|
|
|
/* Wait until new job is started
|
|
*/
|
|
static SANE_Status
|
|
device_start_wait (device *dev)
|
|
{
|
|
for (;;) {
|
|
DEVICE_STM_STATE state = device_stm_state_get(dev);
|
|
|
|
switch (state) {
|
|
case DEVICE_STM_IDLE:
|
|
break;
|
|
|
|
case DEVICE_STM_SCANNING:
|
|
if (dev->proto_ctx.location != NULL) {
|
|
return SANE_STATUS_GOOD;
|
|
}
|
|
break;
|
|
|
|
case DEVICE_STM_DONE:
|
|
return dev->job_status;
|
|
|
|
default:
|
|
return SANE_STATUS_GOOD;
|
|
}
|
|
|
|
eloop_cond_wait(&dev->stm_cond);
|
|
}
|
|
}
|
|
|
|
/* Enforce CONFIG_START_RETRY_INTERVAL
|
|
*/
|
|
static void
|
|
device_start_retry_pause (device *dev)
|
|
{
|
|
struct timespec now;
|
|
int64_t pause_us;
|
|
|
|
clock_gettime(CLOCK_MONOTONIC, &now);
|
|
|
|
pause_us = (int64_t) (now.tv_sec - dev->stm_last_fail_time.tv_sec) *
|
|
1000000;
|
|
pause_us += (int64_t) (now.tv_nsec - dev->stm_last_fail_time.tv_nsec) /
|
|
1000;
|
|
pause_us = (int64_t) (CONFIG_START_RETRY_INTERVAL * 1000) - pause_us;
|
|
|
|
if (pause_us > 1000) {
|
|
log_debug(dev->log, "sane_start() retried too often; pausing for %d ms",
|
|
(int) (pause_us / 1000));
|
|
|
|
eloop_mutex_unlock();
|
|
usleep((useconds_t) pause_us);
|
|
eloop_mutex_lock();
|
|
}
|
|
}
|
|
|
|
/* Start new scanning job
|
|
*/
|
|
static SANE_Status
|
|
device_start_new_job (device *dev)
|
|
{
|
|
SANE_Status status;
|
|
|
|
device_start_retry_pause(dev);
|
|
|
|
dev->stm_cancel_sent = false;
|
|
dev->job_status = SANE_STATUS_GOOD;
|
|
mem_free((char*) dev->proto_ctx.location);
|
|
dev->proto_ctx.location = NULL;
|
|
dev->proto_ctx.failed_op = PROTO_OP_NONE;
|
|
dev->proto_ctx.failed_attempt = 0;
|
|
dev->proto_ctx.images_received = 0;
|
|
|
|
eloop_call(device_start_do, dev);
|
|
|
|
log_debug(dev->log, "device_start_wait: waiting");
|
|
status = device_start_wait(dev);
|
|
log_debug(dev->log, "device_start_wait: %s", sane_strstatus(status));
|
|
|
|
switch (status) {
|
|
case SANE_STATUS_GOOD:
|
|
case SANE_STATUS_CANCELLED:
|
|
memset(&dev->stm_last_fail_time, 0, sizeof(dev->stm_last_fail_time));
|
|
break;
|
|
|
|
default:
|
|
clock_gettime(CLOCK_MONOTONIC, &dev->stm_last_fail_time);
|
|
}
|
|
|
|
if (status == SANE_STATUS_GOOD) {
|
|
dev->flags |= DEVICE_READING;
|
|
} else {
|
|
dev->flags &= ~DEVICE_SCANNING;
|
|
if (device_stm_state_get(dev) == DEVICE_STM_DONE) {
|
|
device_stm_state_set(dev, DEVICE_STM_IDLE);
|
|
}
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
/* Start scanning operation
|
|
*/
|
|
SANE_Status
|
|
device_start (device *dev)
|
|
{
|
|
/* Already scanning? */
|
|
if ((dev->flags & DEVICE_SCANNING) != 0) {
|
|
log_debug(dev->log, "device_start: already scanning");
|
|
return SANE_STATUS_INVAL;
|
|
}
|
|
|
|
/* Don's start if window is not valid */
|
|
if (dev->opt.params.lines == 0 || dev->opt.params.pixels_per_line == 0) {
|
|
log_debug(dev->log, "device_start: invalid scan window");
|
|
return SANE_STATUS_INVAL;
|
|
}
|
|
|
|
/* Update state */
|
|
dev->flags |= DEVICE_SCANNING;
|
|
pollable_reset(dev->read_pollable);
|
|
dev->read_non_blocking = SANE_FALSE;
|
|
|
|
/* Scanner idle? Start new job */
|
|
if (device_stm_state_get(dev) == DEVICE_STM_IDLE) {
|
|
return device_start_new_job(dev);
|
|
}
|
|
|
|
/* Previous job still running. Synchronize with it
|
|
*/
|
|
while (device_stm_state_working(dev)
|
|
&& http_data_queue_len(dev->read_queue) == 0) {
|
|
log_debug(dev->log, "device_start: waiting for background scan job");
|
|
eloop_cond_wait(&dev->stm_cond);
|
|
}
|
|
|
|
/* If we have more buffered images, just start
|
|
* decoding the next one
|
|
*/
|
|
if (http_data_queue_len(dev->read_queue) > 0) {
|
|
dev->flags |= DEVICE_READING;
|
|
pollable_signal(dev->read_pollable);
|
|
return SANE_STATUS_GOOD;
|
|
}
|
|
|
|
/* Seems that previous job has finished.
|
|
*
|
|
* If it failed by itself (but not cancelled), return its status now.
|
|
* Otherwise, start new job
|
|
*/
|
|
log_assert (dev->log, device_stm_state_get(dev) == DEVICE_STM_DONE);
|
|
|
|
device_stm_state_set(dev, DEVICE_STM_IDLE);
|
|
if (dev->job_status != SANE_STATUS_GOOD &&
|
|
dev->job_status != SANE_STATUS_CANCELLED) {
|
|
dev->flags &= ~DEVICE_SCANNING;
|
|
return dev->job_status;
|
|
}
|
|
|
|
/* Start new scan job */
|
|
return device_start_new_job(dev);
|
|
}
|
|
|
|
/* Cancel scanning operation
|
|
*/
|
|
void
|
|
device_cancel (device *dev)
|
|
{
|
|
/* Note, xsane calls sane_cancel() after each successful
|
|
* scan "just in case", which kills scan job running in
|
|
* background. So ignore cancel request, if from the API
|
|
* point of view we are not "scanning" (i.e., not between
|
|
* sane_start() and sane_read() completion)
|
|
*/
|
|
if ((dev->flags & DEVICE_SCANNING) == 0) {
|
|
return;
|
|
}
|
|
|
|
device_stm_cancel_req(dev, NULL);
|
|
}
|
|
|
|
/* Set I/O mode
|
|
*/
|
|
SANE_Status
|
|
device_set_io_mode (device *dev, SANE_Bool non_blocking)
|
|
{
|
|
if ((dev->flags & DEVICE_SCANNING) == 0) {
|
|
log_debug(dev->log, "device_set_io_mode: not scanning");
|
|
return SANE_STATUS_INVAL;
|
|
}
|
|
|
|
dev->read_non_blocking = non_blocking;
|
|
return SANE_STATUS_GOOD;
|
|
}
|
|
|
|
/* Get select file descriptor
|
|
*/
|
|
SANE_Status
|
|
device_get_select_fd (device *dev, SANE_Int *fd)
|
|
{
|
|
if ((dev->flags & DEVICE_SCANNING) == 0) {
|
|
log_debug(dev->log, "device_get_select_fd: not scanning");
|
|
return SANE_STATUS_INVAL;
|
|
}
|
|
|
|
*fd = pollable_get_fd(dev->read_pollable);
|
|
return SANE_STATUS_GOOD;
|
|
}
|
|
|
|
|
|
/******************** Read machinery ********************/
|
|
/* Setup read_filters
|
|
*/
|
|
static void
|
|
device_read_filters_setup (device *dev)
|
|
{
|
|
device_read_filters_cleanup(dev);
|
|
dev->read_filters = filter_chain_push_xlat(NULL, &dev->opt);
|
|
filter_chain_dump(dev->read_filters, dev->log);
|
|
}
|
|
|
|
/* Cleanup read_filters
|
|
*/
|
|
static void
|
|
device_read_filters_cleanup (device *dev)
|
|
{
|
|
filter_chain_free(dev->read_filters);
|
|
dev->read_filters = NULL;
|
|
}
|
|
|
|
/* Pull next image from the read queue and start decoding
|
|
*/
|
|
static SANE_Status
|
|
device_read_next (device *dev)
|
|
{
|
|
error err;
|
|
size_t line_capacity;
|
|
SANE_Parameters params;
|
|
image_decoder *decoder;
|
|
int wid, hei;
|
|
int skip_lines = 0;
|
|
|
|
/* Pull next image out of the queue */
|
|
dev->read_image = http_data_queue_pull(dev->read_queue);
|
|
if (dev->read_image == NULL) {
|
|
return SANE_STATUS_EOF;
|
|
}
|
|
|
|
/* Guess format and choose decoder */
|
|
dev->proto_ctx.format_detected =
|
|
image_format_detect(dev->read_image->bytes, dev->read_image->size);
|
|
|
|
if (dev->proto_ctx.format_detected == ID_FORMAT_UNKNOWN) {
|
|
err = eloop_eprintf("Can't detect image format");
|
|
goto DONE;
|
|
}
|
|
|
|
decoder = dev->decoders[dev->proto_ctx.format_detected];
|
|
if (decoder == NULL) {
|
|
err = eloop_eprintf("Unsupported image format \"%s\"",
|
|
id_format_short_name(dev->proto_ctx.format_detected));
|
|
goto DONE;
|
|
}
|
|
|
|
/* Start new image decoding */
|
|
err = image_decoder_begin(decoder,
|
|
dev->read_image->bytes, dev->read_image->size);
|
|
|
|
if (err != NULL) {
|
|
goto DONE;
|
|
}
|
|
|
|
/* Obtain and dump image parameters */
|
|
image_decoder_get_params(decoder, ¶ms);
|
|
|
|
log_trace(dev->log, "==============================");
|
|
log_trace(dev->log, "Image received with the following parameters:");
|
|
log_trace(dev->log, " format: %s",
|
|
id_format_short_name(dev->proto_ctx.format_detected));
|
|
log_trace(dev->log, " content type: %s", image_content_type(decoder));
|
|
log_trace(dev->log, " frame format: %s",
|
|
params.format == SANE_FRAME_GRAY ? "Gray" : "RGB" );
|
|
log_trace(dev->log, " image size: %dx%d", params.pixels_per_line,
|
|
params.lines);
|
|
log_trace(dev->log, " color depth: %d", params.depth);
|
|
log_trace(dev->log, "");
|
|
|
|
/* Validate image parameters */
|
|
dev->read_24_to_8 = false;
|
|
if (params.format == SANE_FRAME_RGB &&
|
|
dev->opt.params.format == SANE_FRAME_GRAY) {
|
|
dev->read_24_to_8 = true;
|
|
log_trace(dev->log, "resampling: RGB24->Grayscale8");
|
|
} else if (params.format != dev->opt.params.format) {
|
|
/* This is what we cannot handle */
|
|
err = ERROR("Unexpected frame format");
|
|
goto DONE;
|
|
}
|
|
|
|
wid = params.pixels_per_line;
|
|
hei = params.lines;
|
|
|
|
/* Setup image clipping
|
|
*
|
|
* The following variants are possible:
|
|
*
|
|
* <------real image size------><--fill-->
|
|
* <---skip---><---returned image size--->
|
|
* <------------line capacity------------>
|
|
*
|
|
* <------------real image size------------>
|
|
* <---skip---><--returned image size-->
|
|
* <-------------line capacity------------->
|
|
*
|
|
* Real image size is a size of image after decoder.
|
|
* Returned image size is a size of image that we
|
|
* return to the client
|
|
*
|
|
* If device for some reasons unable to handle X/Y
|
|
* offset in hardware, we need to skip some bytes (horizontally)
|
|
* or lines (vertically)
|
|
*
|
|
* If real image is smaller that expected, we need to
|
|
* fill some bytes/lines with 0xff
|
|
*
|
|
* Line buffer capacity must be big enough to fit
|
|
* real image size (we promised it do decoder) and
|
|
* returned image size, whatever is large
|
|
*/
|
|
if (dev->job_skip_x >= wid || dev->job_skip_y >= hei) {
|
|
/* Trivial case - just skip everything */
|
|
dev->read_line_end = 0;
|
|
dev->read_skip_bytes = 0;
|
|
dev->read_line_real_wid = 0;
|
|
line_capacity = dev->opt.params.bytes_per_line;
|
|
} else {
|
|
image_window win;
|
|
int bpp = dev->opt.params.format == SANE_FRAME_RGB ? 3 : 1;
|
|
int returned_size_and_skip;
|
|
|
|
win.x_off = dev->job_skip_x;
|
|
win.y_off = dev->job_skip_y;
|
|
win.wid = wid - dev->job_skip_x;
|
|
win.hei = hei - dev->job_skip_y;
|
|
|
|
err = image_decoder_set_window(decoder, &win);
|
|
if (err != NULL) {
|
|
goto DONE;
|
|
}
|
|
|
|
dev->read_skip_bytes = 0;
|
|
if (win.x_off != dev->job_skip_x) {
|
|
dev->read_skip_bytes = bpp * (dev->job_skip_x - win.x_off);
|
|
}
|
|
|
|
if (win.y_off != dev->job_skip_y) {
|
|
skip_lines = dev->job_skip_y - win.y_off;
|
|
}
|
|
|
|
line_capacity = win.wid;
|
|
if (params.format == SANE_FRAME_RGB) {
|
|
line_capacity *= 3;
|
|
}
|
|
|
|
returned_size_and_skip = dev->read_skip_bytes +
|
|
dev->opt.params.bytes_per_line;
|
|
|
|
line_capacity = math_max(line_capacity, returned_size_and_skip);
|
|
dev->read_line_real_wid = win.wid;
|
|
}
|
|
|
|
/* Initialize image decoding */
|
|
dev->read_line_buf = mem_new(SANE_Byte, line_capacity);
|
|
memset(dev->read_line_buf, 0xff, line_capacity);
|
|
|
|
dev->read_line_num = 0;
|
|
dev->read_line_off = dev->opt.params.bytes_per_line;
|
|
dev->read_line_end = hei - skip_lines;
|
|
|
|
for (;skip_lines > 0; skip_lines --) {
|
|
err = image_decoder_read_line(decoder, dev->read_line_buf);
|
|
if (err != NULL) {
|
|
goto DONE;
|
|
}
|
|
}
|
|
|
|
/* Wake up reader */
|
|
pollable_signal(dev->read_pollable);
|
|
|
|
DONE:
|
|
if (err != NULL) {
|
|
log_debug(dev->log, ESTRING(err));
|
|
http_data_unref(dev->read_image);
|
|
dev->read_image = NULL;
|
|
return SANE_STATUS_IO_ERROR;
|
|
}
|
|
|
|
return SANE_STATUS_GOOD;
|
|
}
|
|
|
|
/* Perform 24 to 8 bit image resampling for a single line
|
|
*/
|
|
static void
|
|
device_read_24_to_8_resample (device *dev)
|
|
{
|
|
int i, len = dev->read_line_real_wid;
|
|
uint8_t *in = dev->read_line_buf;
|
|
uint8_t *out = dev->read_line_buf;
|
|
|
|
for (i = 0; i < len; i ++) {
|
|
/* Y = R * 0.299 + G * 0.587 + B * 0.114
|
|
*
|
|
* 16777216 == 1 << 24
|
|
* 16777216 * 0.299 == 5016387.584 ~= 5016387
|
|
* 16777216 * 0.587 == 9848225.792 ~= 9848226
|
|
* 16777216 * 0.114 == 1912602.624 ~= 1912603
|
|
*
|
|
* 5016387 + 9848226 + 1912603 == 16777216
|
|
*/
|
|
unsigned long Y;
|
|
|
|
Y = 5016387 * (unsigned long) *in ++;
|
|
Y += 9848226 * (unsigned long) *in ++;
|
|
Y += 1912603 * (unsigned long) *in ++;
|
|
*out ++ = (Y + (1 << 23)) >> 24;
|
|
}
|
|
|
|
if (len < dev->opt.params.bytes_per_line) {
|
|
memset(dev->read_line_buf + len, 0xff,
|
|
dev->opt.params.bytes_per_line - len);
|
|
}
|
|
}
|
|
|
|
/* Decode next image line
|
|
*
|
|
* Note, actual image size, returned by device, may be slightly different
|
|
* from an image size, computed according to scan options and requested
|
|
* from device. So here we adjust actual image to fit the expected (and
|
|
* promised) parameters.
|
|
*
|
|
* Alternatively, we could make it problem of frontend. But fronends
|
|
* expect image parameters to be accurate just after sane_start() returns,
|
|
* so at this case sane_start() will have to wait a long time until image
|
|
* is fully available. Taking in account that some popular frontends
|
|
* (read "xsane") doesn't allow to cancel scanning before sane_start()
|
|
* return, it is not good from the user experience perspective.
|
|
*/
|
|
static SANE_Status
|
|
device_read_decode_line (device *dev)
|
|
{
|
|
const SANE_Int n = dev->read_line_num;
|
|
image_decoder *decoder = dev->decoders[dev->proto_ctx.format_detected];
|
|
|
|
log_assert(dev->log, decoder != NULL);
|
|
|
|
if (n == dev->opt.params.lines) {
|
|
return SANE_STATUS_EOF;
|
|
}
|
|
|
|
if (n >= dev->read_line_end) {
|
|
memset(dev->read_line_buf + dev->read_skip_bytes, 0xff,
|
|
dev->opt.params.bytes_per_line);
|
|
} else {
|
|
error err = image_decoder_read_line(decoder, dev->read_line_buf);
|
|
|
|
if (err != NULL) {
|
|
log_debug(dev->log, ESTRING(err));
|
|
return SANE_STATUS_IO_ERROR;
|
|
}
|
|
|
|
if (dev->read_24_to_8) {
|
|
device_read_24_to_8_resample(dev);
|
|
}
|
|
}
|
|
|
|
filter_chain_apply(dev->read_filters,
|
|
dev->read_line_buf, dev->opt.params.bytes_per_line);
|
|
|
|
dev->read_line_off = 0;
|
|
dev->read_line_num ++;
|
|
|
|
return SANE_STATUS_GOOD;
|
|
}
|
|
|
|
/* Read scanned image
|
|
*/
|
|
SANE_Status
|
|
device_read (device *dev, SANE_Byte *data, SANE_Int max_len, SANE_Int *len_out)
|
|
{
|
|
SANE_Int len = 0;
|
|
SANE_Status status = SANE_STATUS_GOOD;
|
|
image_decoder *decoder = dev->decoders[dev->proto_ctx.format_detected];
|
|
|
|
if (len_out != NULL) {
|
|
*len_out = 0; /* Must return 0, if status is not GOOD */
|
|
}
|
|
|
|
log_assert(dev->log, decoder != NULL);
|
|
|
|
/* Check device state */
|
|
if ((dev->flags & DEVICE_READING) == 0) {
|
|
log_debug(dev->log, "device_read: not scanning");
|
|
return SANE_STATUS_INVAL;
|
|
}
|
|
|
|
/* Validate arguments */
|
|
if (len_out == NULL) {
|
|
log_debug(dev->log, "device_read: zero output buffer");
|
|
return SANE_STATUS_INVAL;
|
|
}
|
|
|
|
/* Wait until device is ready */
|
|
if (dev->read_image == NULL) {
|
|
while (device_stm_state_working(dev) &&
|
|
http_data_queue_empty(dev->read_queue)) {
|
|
if (dev->read_non_blocking) {
|
|
*len_out = 0;
|
|
return SANE_STATUS_GOOD;
|
|
}
|
|
|
|
eloop_cond_wait(&dev->stm_cond);
|
|
}
|
|
|
|
if (dev->job_status == SANE_STATUS_CANCELLED) {
|
|
status = SANE_STATUS_CANCELLED;
|
|
goto DONE;
|
|
}
|
|
|
|
if (http_data_queue_empty(dev->read_queue)) {
|
|
status = dev->job_status;
|
|
log_assert(dev->log, status != SANE_STATUS_GOOD);
|
|
goto DONE;
|
|
}
|
|
|
|
status = device_read_next(dev);
|
|
if (status != SANE_STATUS_GOOD) {
|
|
goto DONE;
|
|
}
|
|
}
|
|
|
|
/* Read line by line */
|
|
for (len = 0; status == SANE_STATUS_GOOD && len < max_len; ) {
|
|
if (dev->read_line_off == dev->opt.params.bytes_per_line) {
|
|
status = device_read_decode_line(dev);
|
|
} else {
|
|
SANE_Int sz = math_min(max_len - len,
|
|
dev->opt.params.bytes_per_line - dev->read_line_off);
|
|
|
|
memcpy(data, dev->read_line_buf + dev->read_skip_bytes +
|
|
dev->read_line_off, sz);
|
|
|
|
data += sz;
|
|
dev->read_line_off += sz;
|
|
len += sz;
|
|
}
|
|
}
|
|
|
|
if (status == SANE_STATUS_IO_ERROR) {
|
|
device_job_set_status(dev, SANE_STATUS_IO_ERROR);
|
|
device_stm_cancel_req(dev, "I/O error");
|
|
}
|
|
|
|
/* Cleanup and exit */
|
|
DONE:
|
|
if (status == SANE_STATUS_EOF && len > 0) {
|
|
status = SANE_STATUS_GOOD;
|
|
}
|
|
|
|
if (status == SANE_STATUS_GOOD) {
|
|
*len_out = len;
|
|
return SANE_STATUS_GOOD;
|
|
}
|
|
|
|
/* Scan and read finished - cleanup device */
|
|
dev->flags &= ~(DEVICE_SCANNING | DEVICE_READING);
|
|
image_decoder_reset(decoder);
|
|
|
|
if (dev->read_image != NULL) {
|
|
http_data_unref(dev->read_image);
|
|
dev->read_image = NULL;
|
|
}
|
|
mem_free(dev->read_line_buf);
|
|
dev->read_line_buf = NULL;
|
|
|
|
if (device_stm_state_get(dev) == DEVICE_STM_DONE &&
|
|
(status != SANE_STATUS_EOF || dev->job_status == SANE_STATUS_GOOD)) {
|
|
device_stm_state_set(dev, DEVICE_STM_IDLE);
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
/******************** Initialization/cleanup ********************/
|
|
/* Initialize device management
|
|
*/
|
|
SANE_Status
|
|
device_management_init (void)
|
|
{
|
|
device_table = ptr_array_new(device*);
|
|
eloop_add_start_stop_callback(device_management_start_stop);
|
|
|
|
return SANE_STATUS_GOOD;
|
|
}
|
|
|
|
/* Cleanup device management
|
|
*/
|
|
void
|
|
device_management_cleanup (void)
|
|
{
|
|
if (device_table != NULL) {
|
|
log_assert(NULL, mem_len(device_table) == 0);
|
|
mem_free(device_table);
|
|
device_table = NULL;
|
|
}
|
|
}
|
|
|
|
/* Start/stop device management
|
|
*/
|
|
static void
|
|
device_management_start_stop (bool start)
|
|
{
|
|
if (!start) {
|
|
device_table_purge();
|
|
}
|
|
}
|
|
|
|
/* vim:ts=8:sw=4:et
|
|
*/
|