mirror of
https://github.com/cesanta/mongoose.git
synced 2024-12-28 07:28:13 +08:00
Ditch JSON-RPC from mongoose
PUBLISHED_FROM=89b978c02be2f10eb930ff13673d45249fd67763
This commit is contained in:
parent
421b420a0c
commit
b7a0748312
@ -36,7 +36,6 @@ If you are looking for a complete solution with firmware and cloud components, c
|
|||||||
- plain TCP, plain UDP, SSL/TLS (over TCP, one-way or two-way)
|
- plain TCP, plain UDP, SSL/TLS (over TCP, one-way or two-way)
|
||||||
- HTTP client, HTTP server
|
- HTTP client, HTTP server
|
||||||
- WebSocket client, WebSocket server
|
- WebSocket client, WebSocket server
|
||||||
- JSON-RPC client, JSON-RPC server
|
|
||||||
- MQTT client, MQTT broker
|
- MQTT client, MQTT broker
|
||||||
- CoAP client, CoAP server
|
- CoAP client, CoAP server
|
||||||
- DNS client, DNS server, async DNS resolver
|
- DNS client, DNS server, async DNS resolver
|
||||||
|
@ -7,6 +7,5 @@ title: Disabling flags
|
|||||||
- `MG_DISABLE_MQTT` disable MQTT support
|
- `MG_DISABLE_MQTT` disable MQTT support
|
||||||
- `MG_DISABLE_SHA1` disable SHA1 support (used by Websocket)
|
- `MG_DISABLE_SHA1` disable SHA1 support (used by Websocket)
|
||||||
- `MG_DISABLE_MD5` disable MD5 support (used by HTTP auth)
|
- `MG_DISABLE_MD5` disable MD5 support (used by HTTP auth)
|
||||||
- `MG_DISABLE_JSON_RPC` disable JSON-RPC support
|
|
||||||
- `MG_DISABLE_SOCKETPAIR` disable `mg_broadcast()` API
|
- `MG_DISABLE_SOCKETPAIR` disable `mg_broadcast()` API
|
||||||
- `MG_DISABLE_HTTP_KEEP_ALIVE` useful for embedded systems to save resources
|
- `MG_DISABLE_HTTP_KEEP_ALIVE` useful for embedded systems to save resources
|
||||||
|
@ -14,9 +14,9 @@ flags. Also, some preprocessor flags can be used to tune internal Mongoose
|
|||||||
parameters.
|
parameters.
|
||||||
|
|
||||||
To set a preprocessor flag during compile time, use `-D <PREPROCESSOR_FLAG>`
|
To set a preprocessor flag during compile time, use `-D <PREPROCESSOR_FLAG>`
|
||||||
compiler option. For example, to disable both MQTT and JSON-RPC,
|
compiler option. For example, to disable both MQTT and COAP,
|
||||||
compile the application `my_app.c` like this (assumed UNIX system):
|
compile the application `my_app.c` like this (assumed UNIX system):
|
||||||
|
|
||||||
```
|
```
|
||||||
$ cc my_app.c mongoose.c -D MG_DISABLE_MQTT -D MG_DISABLE_JSON_RPC
|
$ cc my_app.c mongoose.c -D MG_DISABLE_MQTT -D MG_DISABLE_COAP
|
||||||
```
|
```
|
||||||
|
@ -3,7 +3,6 @@ items:
|
|||||||
- { type: dir, name: mbuf.h }
|
- { type: dir, name: mbuf.h }
|
||||||
- { type: dir, name: net.h }
|
- { type: dir, name: net.h }
|
||||||
- { type: dir, name: http.h }
|
- { type: dir, name: http.h }
|
||||||
- { type: dir, name: json-rpc.h }
|
|
||||||
- { type: dir, name: dns.h }
|
- { type: dir, name: dns.h }
|
||||||
- { type: dir, name: dns-server.h }
|
- { type: dir, name: dns-server.h }
|
||||||
- { type: dir, name: mqtt.h }
|
- { type: dir, name: mqtt.h }
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
---
|
|
||||||
title: "JSON-RPC"
|
|
||||||
symbol_kind: "intro"
|
|
||||||
decl_name: "json-rpc.h"
|
|
||||||
items:
|
|
||||||
- { type: file, name: mg_rpc_parse_reply.md }
|
|
||||||
- { type: file, name: mg_rpc_create_request.md }
|
|
||||||
- { type: file, name: mg_rpc_create_reply.md }
|
|
||||||
- { type: file, name: mg_rpc_create_error.md }
|
|
||||||
- { type: file, name: mg_rpc_create_std_error.md }
|
|
||||||
- { type: file, name: mg_rpc_dispatch.md }
|
|
||||||
- { type: file, name: struct_mg_rpc_request.md }
|
|
||||||
- { type: file, name: struct_mg_rpc_reply.md }
|
|
||||||
- { type: file, name: struct_mg_rpc_error.md }
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
|||||||
---
|
|
||||||
title: "mg_rpc_create_error()"
|
|
||||||
decl_name: "mg_rpc_create_error"
|
|
||||||
symbol_kind: "func"
|
|
||||||
signature: |
|
|
||||||
int mg_rpc_create_error(char *buf, int len, struct mg_rpc_request *req,
|
|
||||||
int code, const char *message, const char *fmt, ...);
|
|
||||||
---
|
|
||||||
|
|
||||||
Create JSON-RPC error reply in a given buffer.
|
|
||||||
|
|
||||||
Return length of the error, which
|
|
||||||
can be larger then `len` that indicates an overflow.
|
|
||||||
`fmt` format string should conform to `json_emit()` API,
|
|
||||||
see https://github.com/cesanta/frozen
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
|||||||
---
|
|
||||||
title: "mg_rpc_create_reply()"
|
|
||||||
decl_name: "mg_rpc_create_reply"
|
|
||||||
symbol_kind: "func"
|
|
||||||
signature: |
|
|
||||||
int mg_rpc_create_reply(char *buf, int len, const struct mg_rpc_request *req,
|
|
||||||
const char *result_fmt, ...);
|
|
||||||
---
|
|
||||||
|
|
||||||
Create JSON-RPC reply in a given buffer.
|
|
||||||
|
|
||||||
Return length of the reply, which
|
|
||||||
can be larger then `len` that indicates an overflow.
|
|
||||||
`result_fmt` format string should conform to `json_emit()` API,
|
|
||||||
see https://github.com/cesanta/frozen
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
|||||||
---
|
|
||||||
title: "mg_rpc_create_request()"
|
|
||||||
decl_name: "mg_rpc_create_request"
|
|
||||||
symbol_kind: "func"
|
|
||||||
signature: |
|
|
||||||
int mg_rpc_create_request(char *buf, int len, const char *method,
|
|
||||||
const char *id, const char *params_fmt, ...);
|
|
||||||
---
|
|
||||||
|
|
||||||
Create JSON-RPC request in a given buffer.
|
|
||||||
|
|
||||||
Return length of the request, which
|
|
||||||
can be larger then `len` that indicates an overflow.
|
|
||||||
`params_fmt` format string should conform to `json_emit()` API,
|
|
||||||
see https://github.com/cesanta/frozen
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
|||||||
---
|
|
||||||
title: "mg_rpc_create_std_error()"
|
|
||||||
decl_name: "mg_rpc_create_std_error"
|
|
||||||
symbol_kind: "func"
|
|
||||||
signature: |
|
|
||||||
int mg_rpc_create_std_error(char *buf, int len, struct mg_rpc_request *req,
|
|
||||||
int code);
|
|
||||||
---
|
|
||||||
|
|
||||||
Create JSON-RPC error in a given buffer.
|
|
||||||
|
|
||||||
Return length of the error, which
|
|
||||||
can be larger then `len` that indicates an overflow. See
|
|
||||||
JSON_RPC_*_ERROR definitions for standard error values:
|
|
||||||
|
|
||||||
- `#define JSON_RPC_PARSE_ERROR (-32700)`
|
|
||||||
- `#define JSON_RPC_INVALID_REQUEST_ERROR (-32600)`
|
|
||||||
- `#define JSON_RPC_METHOD_NOT_FOUND_ERROR (-32601)`
|
|
||||||
- `#define JSON_RPC_INVALID_PARAMS_ERROR (-32602)`
|
|
||||||
- `#define JSON_RPC_INTERNAL_ERROR (-32603)`
|
|
||||||
- `#define JSON_RPC_SERVER_ERROR (-32000)`
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
title: "mg_rpc_dispatch()"
|
|
||||||
decl_name: "mg_rpc_dispatch"
|
|
||||||
symbol_kind: "func"
|
|
||||||
signature: |
|
|
||||||
int mg_rpc_dispatch(const char *buf, int, char *dst, int dst_len,
|
|
||||||
const char **methods, mg_rpc_handler_t *handlers);
|
|
||||||
---
|
|
||||||
|
|
||||||
Dispatches a JSON-RPC request.
|
|
||||||
|
|
||||||
Parses JSON-RPC request contained in `buf`, `len`.
|
|
||||||
Then, dispatches the request to the correct handler method.
|
|
||||||
Valid method names should be specified in NULL
|
|
||||||
terminated array `methods`, and corresponding handlers in `handlers`.
|
|
||||||
Result is put in `dst`, `dst_len`. Return: length of the result, which
|
|
||||||
can be larger then `dst_len` that indicates an overflow.
|
|
||||||
Overflown bytes are not written to the buffer.
|
|
||||||
If method is not found, an error is automatically generated.
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
|||||||
---
|
|
||||||
title: "mg_rpc_parse_reply()"
|
|
||||||
decl_name: "mg_rpc_parse_reply"
|
|
||||||
symbol_kind: "func"
|
|
||||||
signature: |
|
|
||||||
int mg_rpc_parse_reply(const char *buf, int len, struct json_token *toks,
|
|
||||||
int max_toks, struct mg_rpc_reply *,
|
|
||||||
struct mg_rpc_error *);
|
|
||||||
---
|
|
||||||
|
|
||||||
Parse JSON-RPC reply contained in `buf`, `len` into JSON tokens array
|
|
||||||
`toks`, `max_toks`. If buffer contains valid reply, `reply` structure is
|
|
||||||
populated. The result of RPC call is located in `reply.result`. On error,
|
|
||||||
`error` structure is populated. Returns: the result of calling
|
|
||||||
`parse_json(buf, len, toks, max_toks)`:
|
|
||||||
|
|
||||||
On success, an offset inside `json_string` is returned
|
|
||||||
where parsing has finished. On failure, a negative number is
|
|
||||||
returned, one of:
|
|
||||||
|
|
||||||
- `#define JSON_STRING_INVALID -1`
|
|
||||||
- `#define JSON_STRING_INCOMPLETE -2`
|
|
||||||
- `#define JSON_TOKEN_ARRAY_TOO_SMALL -3`
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
|||||||
---
|
|
||||||
title: "struct mg_rpc_error"
|
|
||||||
decl_name: "struct mg_rpc_error"
|
|
||||||
symbol_kind: "struct"
|
|
||||||
signature: |
|
|
||||||
struct mg_rpc_error {
|
|
||||||
struct json_token *message; /* Whole RPC message */
|
|
||||||
struct json_token *id; /* Message ID */
|
|
||||||
struct json_token *error_code; /* error.code */
|
|
||||||
struct json_token *error_message; /* error.message */
|
|
||||||
struct json_token *error_data; /* error.data, can be NULL */
|
|
||||||
};
|
|
||||||
---
|
|
||||||
|
|
||||||
JSON-RPC error
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
|||||||
---
|
|
||||||
title: "struct mg_rpc_reply"
|
|
||||||
decl_name: "struct mg_rpc_reply"
|
|
||||||
symbol_kind: "struct"
|
|
||||||
signature: |
|
|
||||||
struct mg_rpc_reply {
|
|
||||||
struct json_token *message; /* Whole RPC message */
|
|
||||||
struct json_token *id; /* Message ID */
|
|
||||||
struct json_token *result; /* Remote call result */
|
|
||||||
};
|
|
||||||
---
|
|
||||||
|
|
||||||
JSON-RPC response
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
|||||||
---
|
|
||||||
title: "struct mg_rpc_request"
|
|
||||||
decl_name: "struct mg_rpc_request"
|
|
||||||
symbol_kind: "struct"
|
|
||||||
signature: |
|
|
||||||
struct mg_rpc_request {
|
|
||||||
struct json_token *message; /* Whole RPC message */
|
|
||||||
struct json_token *id; /* Message ID */
|
|
||||||
struct json_token *method; /* Method name */
|
|
||||||
struct json_token *params; /* Method params */
|
|
||||||
};
|
|
||||||
---
|
|
||||||
|
|
||||||
JSON-RPC request
|
|
||||||
|
|
@ -4,7 +4,7 @@ title: Overview
|
|||||||
|
|
||||||
Mongoose is a swiss army knife for embedded network programming.
|
Mongoose is a swiss army knife for embedded network programming.
|
||||||
It implements event-driven non-blocking API for TCP, UDP, HTTP,
|
It implements event-driven non-blocking API for TCP, UDP, HTTP,
|
||||||
WebSocket, CoAP, MQTT, JSON-RPC for both client and server mode.
|
WebSocket, CoAP, MQTT for both client and server mode.
|
||||||
Features include:
|
Features include:
|
||||||
|
|
||||||
- Cross-platform: works on Linux/UNIX, MacOS, QNX, eCos, Windows, Android,
|
- Cross-platform: works on Linux/UNIX, MacOS, QNX, eCos, Windows, Android,
|
||||||
@ -16,7 +16,6 @@ Features include:
|
|||||||
- plain TCP, plain UDP, SSL/TLS (over TCP, one-way or two-way)
|
- plain TCP, plain UDP, SSL/TLS (over TCP, one-way or two-way)
|
||||||
- HTTP client and server
|
- HTTP client and server
|
||||||
- WebSocket client and server
|
- WebSocket client and server
|
||||||
- JSON-RPC client and server
|
|
||||||
- MQTT client and server
|
- MQTT client and server
|
||||||
- CoAP client and server
|
- CoAP client and server
|
||||||
- DNS client and server
|
- DNS client and server
|
||||||
|
@ -95,27 +95,19 @@ static double send_acc_data_since(struct mg_connection *nc,
|
|||||||
|
|
||||||
static void process_command(struct mg_connection *nc, unsigned char *data,
|
static void process_command(struct mg_connection *nc, unsigned char *data,
|
||||||
size_t len) {
|
size_t len) {
|
||||||
struct json_token *toks = parse_json2((const char *) data, len);
|
// TODO(lsm): use proper JSON parser
|
||||||
if (toks == NULL) {
|
int cmd, n, val;
|
||||||
|
double t;
|
||||||
|
if (sscanf((char *) data, "{\t\": %d, \"ts\": %lf, %n", &cmd, &t, &n) != 2) {
|
||||||
LOG(LL_ERROR, ("Invalid command: %.*s", (int) len, data));
|
LOG(LL_ERROR, ("Invalid command: %.*s", (int) len, data));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
struct json_token *t = find_json_token(toks, "t");
|
if (t == 1) {
|
||||||
if (t == NULL) {
|
if (sscanf((char *) data + n, "\"v\": %d", &val) != 1) {
|
||||||
LOG(LL_ERROR, ("Missing type field: %.*s", (int) len, data));
|
|
||||||
goto out_free;
|
|
||||||
}
|
|
||||||
if (t->len == 1 && *t->ptr == '1') {
|
|
||||||
struct json_token *v = find_json_token(toks, "v");
|
|
||||||
if (v == NULL) {
|
|
||||||
LOG(LL_ERROR, ("Missing value: %.*s", (int) len, data));
|
LOG(LL_ERROR, ("Missing value: %.*s", (int) len, data));
|
||||||
goto out_free;
|
return;
|
||||||
}
|
}
|
||||||
if (v->len != 1) {
|
switch (val) {
|
||||||
LOG(LL_ERROR, ("Invalid value: %.*s", (int) len, data));
|
|
||||||
goto out_free;
|
|
||||||
}
|
|
||||||
switch (*v->ptr) {
|
|
||||||
case '0': {
|
case '0': {
|
||||||
GPIO_IF_LedOff(MCU_RED_LED_GPIO);
|
GPIO_IF_LedOff(MCU_RED_LED_GPIO);
|
||||||
break;
|
break;
|
||||||
@ -130,15 +122,13 @@ static void process_command(struct mg_connection *nc, unsigned char *data,
|
|||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
LOG(LL_ERROR, ("Invalid value: %.*s", (int) len, data));
|
LOG(LL_ERROR, ("Invalid value: %.*s", (int) len, data));
|
||||||
goto out_free;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LOG(LL_ERROR, ("Unknown command: %.*s", (int) t->len, t->ptr));
|
LOG(LL_ERROR, ("Unknown command: %.*s", (int) len, data));
|
||||||
goto out_free;
|
return;
|
||||||
}
|
}
|
||||||
out_free:
|
|
||||||
free(toks);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void data_conn_handler(struct mg_connection *nc, int ev, void *ev_data) {
|
void data_conn_handler(struct mg_connection *nc, int ev, void *ev_data) {
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
PROG = json_rpc_server
|
|
||||||
include ../examples.mk
|
|
@ -1,68 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2014 Cesanta Software Limited
|
|
||||||
* All rights reserved
|
|
||||||
*
|
|
||||||
* To test this server, do
|
|
||||||
* $ curl -d '{"id":1,method:"sum",params:[22,33]}' 127.0.0.1:8000
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "mongoose.h"
|
|
||||||
|
|
||||||
static const char *s_http_port = "8000";
|
|
||||||
|
|
||||||
static int rpc_sum(char *buf, int len, struct mg_rpc_request *req) {
|
|
||||||
double sum = 0;
|
|
||||||
int i;
|
|
||||||
|
|
||||||
if (req->params[0].type != JSON_TYPE_ARRAY) {
|
|
||||||
return mg_rpc_create_std_error(buf, len, req,
|
|
||||||
JSON_RPC_INVALID_PARAMS_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (i = 0; i < req->params[0].num_desc; i++) {
|
|
||||||
if (req->params[i + 1].type != JSON_TYPE_NUMBER) {
|
|
||||||
return mg_rpc_create_std_error(buf, len, req,
|
|
||||||
JSON_RPC_INVALID_PARAMS_ERROR);
|
|
||||||
}
|
|
||||||
sum += strtod(req->params[i + 1].ptr, NULL);
|
|
||||||
}
|
|
||||||
return mg_rpc_create_reply(buf, len, req, "f", sum);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void ev_handler(struct mg_connection *nc, int ev, void *ev_data) {
|
|
||||||
struct http_message *hm = (struct http_message *) ev_data;
|
|
||||||
static const char *methods[] = {"sum", NULL};
|
|
||||||
static mg_rpc_handler_t handlers[] = {rpc_sum, NULL};
|
|
||||||
char buf[100];
|
|
||||||
|
|
||||||
switch (ev) {
|
|
||||||
case MG_EV_HTTP_REQUEST:
|
|
||||||
mg_rpc_dispatch(hm->body.p, hm->body.len, buf, sizeof(buf), methods,
|
|
||||||
handlers);
|
|
||||||
mg_printf(nc,
|
|
||||||
"HTTP/1.0 200 OK\r\nContent-Length: %d\r\n"
|
|
||||||
"Content-Type: application/json\r\n\r\n%s",
|
|
||||||
(int) strlen(buf), buf);
|
|
||||||
nc->flags |= MG_F_SEND_AND_CLOSE;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void) {
|
|
||||||
struct mg_mgr mgr;
|
|
||||||
struct mg_connection *nc;
|
|
||||||
|
|
||||||
mg_mgr_init(&mgr, NULL);
|
|
||||||
nc = mg_bind(&mgr, s_http_port, ev_handler);
|
|
||||||
mg_set_protocol_http_websocket(nc);
|
|
||||||
|
|
||||||
printf("Starting JSON-RPC server on port %s\n", s_http_port);
|
|
||||||
for (;;) {
|
|
||||||
mg_mgr_poll(&mgr, 1000);
|
|
||||||
}
|
|
||||||
mg_mgr_free(&mgr);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
NS=../../mongoose.c
|
|
||||||
FLAGS = ../../mongoose.c -I../..
|
|
||||||
CFLAGS=-W -Wall -DMG_ENABLE_THREADS -pthread $(CFLAGS_EXTRA)
|
|
||||||
PROGS = device_side cloud_side
|
|
||||||
|
|
||||||
all: $(PROGS)
|
|
||||||
|
|
||||||
device_side: Makefile device_side.c $(NS)
|
|
||||||
$(CC) device_side.c $(FLAGS) -o $@ $(CFLAGS)
|
|
||||||
|
|
||||||
cloud_side: Makefile cloud_side.c $(NS)
|
|
||||||
$(CC) cloud_side.c $(FLAGS) -o $@ $(CFLAGS)
|
|
||||||
|
|
||||||
device_side.exe: Makefile device_side.c $(NS)
|
|
||||||
cl device_side.c $(FLAGS) /MD /Fe$@
|
|
||||||
|
|
||||||
clean:
|
|
||||||
rm -rf *.gc* *.dSYM *.exe *.obj *.o a.out $(PROGS)
|
|
@ -1,72 +0,0 @@
|
|||||||
= Raspberry Pi camera/LED demo
|
|
||||||
|
|
||||||
== Overview
|
|
||||||
|
|
||||||
The link:/[demo] consists of web app providing access to a webcam and a LED attached to a RaspberryPi.
|
|
||||||
The device is assumed to have a limited bandwidth towards the server hosting the web app.
|
|
||||||
|
|
||||||
== Objective
|
|
||||||
|
|
||||||
The demo shows how to use websockets to communicate bidirectionally with an embedded device using standard protocols.
|
|
||||||
|
|
||||||
It also shows that it's possible to use Smart.c to develop also the cloud endpoint and expose WebSocket and RESTful APIs
|
|
||||||
easy to integreate with modern web stacks.
|
|
||||||
|
|
||||||
== How it works
|
|
||||||
|
|
||||||
image::docs/arch.png[]
|
|
||||||
|
|
||||||
There are two components, once with runs on the device (`device_side`) and one that runs on a stronger machine
|
|
||||||
and with more bandwidth (`cloud_side`).
|
|
||||||
|
|
||||||
The device app connects to the cloud app via websocket and sends a new jpeg frame as fast as the underlying `raspistill` camera
|
|
||||||
grabbing application can handle. The device automatically attempts reconnecting.
|
|
||||||
|
|
||||||
The cloud side serves the webapp static pages and serves an MPJEG image on `/mpjg`.
|
|
||||||
The MPJEG image handler blocks all the clients until a JPEG frame arrives via websocket
|
|
||||||
and then every client will receive a copy of the frame.
|
|
||||||
|
|
||||||
The web app can turn on and off the LED via a RESTful api accessible via the `/api` handler.
|
|
||||||
|
|
||||||
== Installation
|
|
||||||
|
|
||||||
=== Server side
|
|
||||||
|
|
||||||
----
|
|
||||||
git clone https://github.com/cesanta/mongoose
|
|
||||||
cd mongoose/examples/web_demo
|
|
||||||
make cloud_side && ./cloud_side 0.0.0.0:8080
|
|
||||||
----
|
|
||||||
|
|
||||||
=== Raspberry Pi
|
|
||||||
|
|
||||||
The instructions provided here are tailored for the Raspbian distribution.
|
|
||||||
|
|
||||||
==== Dependencies
|
|
||||||
|
|
||||||
jpegoptim::
|
|
||||||
apt-get install jpegoptim
|
|
||||||
|
|
||||||
camera::
|
|
||||||
run raspi-config and enable camera
|
|
||||||
|
|
||||||
==== LED
|
|
||||||
|
|
||||||
In order to access the led on your link:http://www.qdh.org.uk/wordpress/?page_id=15[HotPi]
|
|
||||||
board you need to export the gpio pins:
|
|
||||||
|
|
||||||
----
|
|
||||||
for i in 22 23 24; do
|
|
||||||
echo $i >/sys/class/gpio/export
|
|
||||||
echo out >/sys/class/gpio/gpio$i/direction
|
|
||||||
chgrp pi /sys/class/gpio/gpio$i/value
|
|
||||||
done
|
|
||||||
----
|
|
||||||
|
|
||||||
==== Build and run
|
|
||||||
|
|
||||||
----
|
|
||||||
git clone https://github.com/cesanta/mongoose
|
|
||||||
cd mongoose/examples/web_demo
|
|
||||||
make device_side && ./device_side yourserver:8080
|
|
||||||
----
|
|
@ -1,146 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2014 Cesanta Software Limited
|
|
||||||
* All rights reserved
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This is the cloud endpoint of the Raspberry Pi camera/LED example
|
|
||||||
* of the Mongoose networking library.
|
|
||||||
* It is a simple web server, serving both static files, a REST API handler,
|
|
||||||
* and a WebSocket handler.
|
|
||||||
*/
|
|
||||||
#include "mongoose.h"
|
|
||||||
|
|
||||||
static struct mg_serve_http_opts web_root_opts;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Forwards the jpeg frame data to all open mjpeg connections.
|
|
||||||
*
|
|
||||||
* Incoming messages follow a very simple binary frame format:
|
|
||||||
* 4 bytes: timestamp (in network byte order)
|
|
||||||
* n bytes: jpeg payload
|
|
||||||
*
|
|
||||||
* The timestamp is used to compute a lag.
|
|
||||||
* It's done in a quite stupid way as it requires the device clock
|
|
||||||
* to be synchronized with the cloud endpoint.
|
|
||||||
*/
|
|
||||||
static void push_frame_to_clients(struct mg_mgr *mgr,
|
|
||||||
const struct websocket_message *wm) {
|
|
||||||
struct mg_connection *nc;
|
|
||||||
/*
|
|
||||||
* mjpeg connections are tagged with the MG_F_USER_2 flag so we can find them
|
|
||||||
* my scanning the connection list provided by the mongoose manager.
|
|
||||||
*/
|
|
||||||
for (nc = mg_next(mgr, NULL); nc != NULL; nc = mg_next(mgr, nc)) {
|
|
||||||
if (!(nc->flags & MG_F_USER_2)) continue; // Ignore un-marked requests
|
|
||||||
|
|
||||||
mg_printf(nc,
|
|
||||||
"--w00t\r\nContent-Type: image/jpeg\r\n"
|
|
||||||
"Content-Length: %lu\r\n\r\n",
|
|
||||||
(unsigned long) wm->size);
|
|
||||||
mg_send(nc, wm->data, wm->size);
|
|
||||||
mg_send(nc, "\r\n", 2);
|
|
||||||
printf("Image pushed to %p\n", nc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Forwards API payload to the device, by scanning through
|
|
||||||
* all the connections to find those that are tagged as WebSocket.
|
|
||||||
*/
|
|
||||||
static void send_command_to_the_device(struct mg_mgr *mgr,
|
|
||||||
const struct mg_str *cmd) {
|
|
||||||
struct mg_connection *nc;
|
|
||||||
for (nc = mg_next(mgr, NULL); nc != NULL; nc = mg_next(mgr, nc)) {
|
|
||||||
if (!(nc->flags & MG_F_IS_WEBSOCKET))
|
|
||||||
continue; // Ignore non-websocket requests
|
|
||||||
|
|
||||||
mg_send_websocket_frame(nc, WEBSOCKET_OP_TEXT, cmd->p, cmd->len);
|
|
||||||
printf("Sent API command [%.*s] to %p\n", (int) cmd->len, cmd->p, nc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Main event handler. Receives data events and dispatches to
|
|
||||||
* the appropriate handler function.
|
|
||||||
*
|
|
||||||
* 1. RESTful API requests are handled by send_command_to_the_device.
|
|
||||||
* 2. requests to /mpeg are established and left open waiting for data to arrive
|
|
||||||
* from WebSocket.
|
|
||||||
* 3. WebSocket frames are handled by push_frame_to_clients.
|
|
||||||
* 4. All other connections are passed to the mg_serve_http handler
|
|
||||||
* which serves static files.
|
|
||||||
*/
|
|
||||||
static void ev_handler(struct mg_connection *nc, int ev, void *ev_data) {
|
|
||||||
struct websocket_message *wm = (struct websocket_message *) ev_data;
|
|
||||||
struct http_message *hm = (struct http_message *) ev_data;
|
|
||||||
|
|
||||||
switch (ev) {
|
|
||||||
case MG_EV_HTTP_REQUEST:
|
|
||||||
if (mg_vcmp(&hm->uri, "/mjpg") == 0) {
|
|
||||||
nc->flags |= MG_F_USER_2; /* Set a mark on image requests */
|
|
||||||
mg_printf(nc, "%s",
|
|
||||||
"HTTP/1.0 200 OK\r\n"
|
|
||||||
"Cache-Control: no-cache\r\n"
|
|
||||||
"Pragma: no-cache\r\n"
|
|
||||||
"Expires: Thu, 01 Dec 1994 16:00:00 GMT\r\n"
|
|
||||||
"Connection: close\r\n"
|
|
||||||
"Content-Type: multipart/x-mixed-replace; "
|
|
||||||
"boundary=--w00t\r\n\r\n");
|
|
||||||
} else if (mg_vcmp(&hm->uri, "/api") == 0 && hm->body.len > 0) {
|
|
||||||
/*
|
|
||||||
* RESTful API call. HTTP message body should be a JSON message.
|
|
||||||
* We should parse it and take appropriate action.
|
|
||||||
* In our case, simply forward that call to the device.
|
|
||||||
*/
|
|
||||||
printf("API CALL: [%.*s] [%.*s]\n", (int) hm->method.len, hm->method.p,
|
|
||||||
(int) hm->body.len, hm->body.p);
|
|
||||||
send_command_to_the_device(nc->mgr, &hm->body);
|
|
||||||
mg_printf(nc, "HTTP/1.0 200 OK\nContent-Length: 0\n\n");
|
|
||||||
} else {
|
|
||||||
/* Delegate to the static web server handler for all other paths. */
|
|
||||||
mg_serve_http(nc, hm, web_root_opts);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case MG_EV_WEBSOCKET_FRAME:
|
|
||||||
printf("Got websocket frame, size %lu\n", (unsigned long) wm->size);
|
|
||||||
push_frame_to_clients(nc->mgr, wm);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
|
||||||
struct mg_mgr mgr;
|
|
||||||
struct mg_connection *nc;
|
|
||||||
|
|
||||||
if (argc != 2) {
|
|
||||||
fprintf(stderr, "Usage: %s <listening_addr>\n", argv[0]);
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
|
|
||||||
printf("Listening on: [%s]\n", argv[1]);
|
|
||||||
mg_mgr_init(&mgr, NULL);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* mg_bind() creates a listening connection on a given ip:port and
|
|
||||||
* with an attached event handler.
|
|
||||||
* The event handler will only trigger TCP events until the http
|
|
||||||
* protocol handler is installed.
|
|
||||||
*/
|
|
||||||
if ((nc = mg_bind(&mgr, argv[1], ev_handler)) == NULL) {
|
|
||||||
fprintf(stderr, "Error binding to %s\n", argv[1]);
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
mg_set_protocol_http_websocket(nc);
|
|
||||||
web_root_opts.document_root = "./web_root";
|
|
||||||
|
|
||||||
/*
|
|
||||||
* We explicitly hand over control to the Mongoose manager
|
|
||||||
* in this event loop and we can easily multiplex other activities.
|
|
||||||
*/
|
|
||||||
for (;;) {
|
|
||||||
mg_mgr_poll(&mgr, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
return EXIT_SUCCESS;
|
|
||||||
}
|
|
@ -1,173 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2014 Cesanta Software Limited
|
|
||||||
* All rights reserved
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This is the device endpoint of the Raspberry Pi camera/LED example
|
|
||||||
* of the Mongoose networking library.
|
|
||||||
* It is a simple websocket client, sending jpeg frames obtained from the
|
|
||||||
* RPi camera and receiving JSON commands through the same WebSocket channel
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <unistd.h>
|
|
||||||
#include "mongoose.h"
|
|
||||||
|
|
||||||
static int s_poll_interval_ms = 100;
|
|
||||||
static int s_still_period = 100;
|
|
||||||
static int s_vertical_flip = 0;
|
|
||||||
static int s_width = 320;
|
|
||||||
static int s_height = 180;
|
|
||||||
static const char *s_mjpg_file = "/var/run/shm/cam.jpg";
|
|
||||||
|
|
||||||
static struct mg_connection *client;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Check if there is a new image available and
|
|
||||||
* send it to the cloud endpoint if the send buffer is not too full.
|
|
||||||
* The image is moved in a new file by the jpeg optimizer function;
|
|
||||||
* this ensures that we will detect a new frame when raspistill writes
|
|
||||||
* it's output file.
|
|
||||||
*/
|
|
||||||
static void send_mjpg_frame(struct mg_connection *nc, const char *file_path) {
|
|
||||||
static int skipped_frames = 0;
|
|
||||||
struct stat st;
|
|
||||||
FILE *fp;
|
|
||||||
|
|
||||||
/* Check file modification time. */
|
|
||||||
if (stat(file_path, &st) == 0) {
|
|
||||||
/* Skip the frame if there is too much unsent data. */
|
|
||||||
if (nc->send_mbuf.len > 256) skipped_frames++;
|
|
||||||
|
|
||||||
/* Read new mjpg frame into a buffer */
|
|
||||||
fp = fopen(file_path, "rb");
|
|
||||||
char buf[st.st_size];
|
|
||||||
fread(buf, 1, sizeof(buf), fp);
|
|
||||||
fclose(fp);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Delete the file so we can detect when raspistill creates a new one.
|
|
||||||
* mtime granularity is only 1s.
|
|
||||||
*/
|
|
||||||
unlink(file_path);
|
|
||||||
|
|
||||||
/* Send those buffer through the websocket connection */
|
|
||||||
mg_send_websocket_frame(nc, WEBSOCKET_OP_BINARY, buf, sizeof(buf));
|
|
||||||
printf("Sent mjpg frame, %lu bytes after skippping %d frames\n",
|
|
||||||
(unsigned long) sizeof(buf), skipped_frames);
|
|
||||||
skipped_frames = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Turn on or off the LED.
|
|
||||||
* The LED in this example is an RGB led, so all the colors have to be set.
|
|
||||||
*/
|
|
||||||
static void set_led(int v) {
|
|
||||||
char cmd[512];
|
|
||||||
snprintf(cmd, sizeof(cmd),
|
|
||||||
"for i in 22 23 24; do"
|
|
||||||
" echo %d >/sys/class/gpio/gpio$i/value; done",
|
|
||||||
v);
|
|
||||||
system(cmd);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Parse control JSON and perform command:
|
|
||||||
* for now only LED on/off is supported.
|
|
||||||
*/
|
|
||||||
static void perform_control_command(const char *data, size_t len) {
|
|
||||||
struct json_token toks[200], *onoff;
|
|
||||||
parse_json(data, len, toks, sizeof(toks));
|
|
||||||
onoff = find_json_token(toks, "onoff");
|
|
||||||
set_led(strncmp("[\"on\"]", onoff->ptr, onoff->len) == 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Main event handler. Sends websocket frames and receives control commands */
|
|
||||||
static void ev_handler(struct mg_connection *nc, int ev, void *ev_data) {
|
|
||||||
struct websocket_message *wm = (struct websocket_message *) ev_data;
|
|
||||||
|
|
||||||
switch (ev) {
|
|
||||||
case MG_EV_CONNECT:
|
|
||||||
printf("Reconnect: %s\n", *(int *) ev_data == 0 ? "ok" : "failed");
|
|
||||||
if (*(int *) ev_data == 0) {
|
|
||||||
/*
|
|
||||||
* Tune the tcp send buffer size, so that we can skip frames
|
|
||||||
* when the connection is congested. This helps maintaining a
|
|
||||||
* reasonable latency.
|
|
||||||
*/
|
|
||||||
int sndbuf_size = 512;
|
|
||||||
if (setsockopt(nc->sock, SOL_SOCKET, SO_SNDBUF, (void *) &sndbuf_size,
|
|
||||||
sizeof(int)) == -1) {
|
|
||||||
perror("failed to tune TCP send buffer size\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
mg_send_websocket_handshake(nc, "/stream", NULL);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case MG_EV_CLOSE:
|
|
||||||
printf("Connection %p closed\n", nc);
|
|
||||||
client = NULL;
|
|
||||||
break;
|
|
||||||
case MG_EV_POLL:
|
|
||||||
send_mjpg_frame(nc, s_mjpg_file);
|
|
||||||
break;
|
|
||||||
case MG_EV_WEBSOCKET_FRAME:
|
|
||||||
printf("Got control command: [%.*s]\n", (int) wm->size, wm->data);
|
|
||||||
perform_control_command((const char *) wm->data, wm->size);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This thread regenerates s_mjpg_file every s_poll_interval_ms milliseconds.
|
|
||||||
* It is Raspberry PI specific, change this function on other systems.
|
|
||||||
*/
|
|
||||||
static void *generate_mjpg_data_thread_func(void *param) {
|
|
||||||
char cmd[400];
|
|
||||||
(void) param;
|
|
||||||
|
|
||||||
snprintf(cmd, sizeof(cmd),
|
|
||||||
"raspistill -w %d -h %d -n -q 100 -tl %d "
|
|
||||||
"-t 999999999 -v %s -o %s >/dev/null 2>&1",
|
|
||||||
s_width, s_height, s_still_period, s_vertical_flip ? "-vf" : "",
|
|
||||||
s_mjpg_file);
|
|
||||||
|
|
||||||
for (;;) {
|
|
||||||
int ret = system(cmd);
|
|
||||||
if (WIFSIGNALED(ret)) exit(1);
|
|
||||||
sleep(1);
|
|
||||||
}
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
|
||||||
struct mg_mgr mgr;
|
|
||||||
char *addr = argv[1];
|
|
||||||
|
|
||||||
if (argc < 2) {
|
|
||||||
fprintf(stderr, "Usage: %s <server_address>\n", argv[0]);
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Start separate thread that generates MJPG data */
|
|
||||||
mg_start_thread(generate_mjpg_data_thread_func, NULL);
|
|
||||||
|
|
||||||
printf("Streaming [%s] to [%s]\n", s_mjpg_file, addr);
|
|
||||||
|
|
||||||
mg_mgr_init(&mgr, NULL);
|
|
||||||
|
|
||||||
for (;;) {
|
|
||||||
mg_mgr_poll(&mgr, s_poll_interval_ms);
|
|
||||||
|
|
||||||
/* Reconnect if disconnected */
|
|
||||||
if (!client) {
|
|
||||||
sleep(1); /* limit reconnections frequency */
|
|
||||||
printf("Reconnecting to %s...\n", addr);
|
|
||||||
client = mg_connect(&mgr, addr, ev_handler);
|
|
||||||
if (client) mg_set_protocol_http_websocket(client);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return EXIT_SUCCESS;
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 40 KiB |
@ -1,533 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=edge"><![endif]-->
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta name="generator" content="Asciidoctor 1.5.1">
|
|
||||||
<title>Raspberry Pi camera/LED demo</title>
|
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic|Noto+Serif:400,400italic,700,700italic|Droid+Sans+Mono:400">
|
|
||||||
<style>
|
|
||||||
/* Asciidoctor default stylesheet | MIT License | http://asciidoctor.org */
|
|
||||||
/* Remove the comments around the @import statement below when using this as a custom stylesheet */
|
|
||||||
/*@import "https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic|Noto+Serif:400,400italic,700,700italic|Droid+Sans+Mono:400";*/
|
|
||||||
article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}
|
|
||||||
audio,canvas,video{display:inline-block}
|
|
||||||
audio:not([controls]){display:none;height:0}
|
|
||||||
[hidden],template{display:none}
|
|
||||||
script{display:none!important}
|
|
||||||
html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}
|
|
||||||
body{margin:0}
|
|
||||||
a{background:transparent}
|
|
||||||
a:focus{outline:thin dotted}
|
|
||||||
a:active,a:hover{outline:0}
|
|
||||||
h1{font-size:2em;margin:.67em 0}
|
|
||||||
abbr[title]{border-bottom:1px dotted}
|
|
||||||
b,strong{font-weight:bold}
|
|
||||||
dfn{font-style:italic}
|
|
||||||
hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}
|
|
||||||
mark{background:#ff0;color:#000}
|
|
||||||
code,kbd,pre,samp{font-family:monospace;font-size:1em}
|
|
||||||
pre{white-space:pre-wrap}
|
|
||||||
q{quotes:"\201C" "\201D" "\2018" "\2019"}
|
|
||||||
small{font-size:80%}
|
|
||||||
sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
|
|
||||||
sup{top:-.5em}
|
|
||||||
sub{bottom:-.25em}
|
|
||||||
img{border:0}
|
|
||||||
svg:not(:root){overflow:hidden}
|
|
||||||
figure{margin:0}
|
|
||||||
fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}
|
|
||||||
legend{border:0;padding:0}
|
|
||||||
button,input,select,textarea{font-family:inherit;font-size:100%;margin:0}
|
|
||||||
button,input{line-height:normal}
|
|
||||||
button,select{text-transform:none}
|
|
||||||
button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}
|
|
||||||
button[disabled],html input[disabled]{cursor:default}
|
|
||||||
input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}
|
|
||||||
input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}
|
|
||||||
input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}
|
|
||||||
button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}
|
|
||||||
textarea{overflow:auto;vertical-align:top}
|
|
||||||
table{border-collapse:collapse;border-spacing:0}
|
|
||||||
*,*:before,*:after{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}
|
|
||||||
html,body{font-size:100%}
|
|
||||||
body{background:#fff;color:rgba(0,0,0,.8);padding:0;margin:0;font-family:"Noto Serif","DejaVu Serif",serif;font-weight:400;font-style:normal;line-height:1;position:relative;cursor:auto}
|
|
||||||
a:hover{cursor:pointer}
|
|
||||||
img,object,embed{max-width:100%;height:auto}
|
|
||||||
object,embed{height:100%}
|
|
||||||
img{-ms-interpolation-mode:bicubic}
|
|
||||||
#map_canvas img,#map_canvas embed,#map_canvas object,.map_canvas img,.map_canvas embed,.map_canvas object{max-width:none!important}
|
|
||||||
.left{float:left!important}
|
|
||||||
.right{float:right!important}
|
|
||||||
.text-left{text-align:left!important}
|
|
||||||
.text-right{text-align:right!important}
|
|
||||||
.text-center{text-align:center!important}
|
|
||||||
.text-justify{text-align:justify!important}
|
|
||||||
.hide{display:none}
|
|
||||||
.antialiased,body{-webkit-font-smoothing:antialiased}
|
|
||||||
img{display:inline-block;vertical-align:middle}
|
|
||||||
textarea{height:auto;min-height:50px}
|
|
||||||
select{width:100%}
|
|
||||||
p.lead,.paragraph.lead>p,#preamble>.sectionbody>.paragraph:first-of-type p{font-size:1.21875em;line-height:1.6}
|
|
||||||
.subheader,.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{line-height:1.45;color:#7a2518;font-weight:400;margin-top:0;margin-bottom:.25em}
|
|
||||||
div,dl,dt,dd,ul,ol,li,h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6,pre,form,p,blockquote,th,td{margin:0;padding:0;direction:ltr}
|
|
||||||
a{color:#2156a5;text-decoration:underline;line-height:inherit}
|
|
||||||
a:hover,a:focus{color:#1d4b8f}
|
|
||||||
a img{border:none}
|
|
||||||
p{font-family:inherit;font-weight:400;font-size:1em;line-height:1.6;margin-bottom:1.25em;text-rendering:optimizeLegibility}
|
|
||||||
p aside{font-size:.875em;line-height:1.35;font-style:italic}
|
|
||||||
h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{font-family:"Open Sans","DejaVu Sans",sans-serif;font-weight:300;font-style:normal;color:#ba3925;text-rendering:optimizeLegibility;margin-top:1em;margin-bottom:.5em;line-height:1.0125em}
|
|
||||||
h1 small,h2 small,h3 small,#toctitle small,.sidebarblock>.content>.title small,h4 small,h5 small,h6 small{font-size:60%;color:#e99b8f;line-height:0}
|
|
||||||
h1{font-size:2.125em}
|
|
||||||
h2{font-size:1.6875em}
|
|
||||||
h3,#toctitle,.sidebarblock>.content>.title{font-size:1.375em}
|
|
||||||
h4,h5{font-size:1.125em}
|
|
||||||
h6{font-size:1em}
|
|
||||||
hr{border:solid #ddddd8;border-width:1px 0 0;clear:both;margin:1.25em 0 1.1875em;height:0}
|
|
||||||
em,i{font-style:italic;line-height:inherit}
|
|
||||||
strong,b{font-weight:bold;line-height:inherit}
|
|
||||||
small{font-size:60%;line-height:inherit}
|
|
||||||
code{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;font-weight:400;color:rgba(0,0,0,.9)}
|
|
||||||
ul,ol,dl{font-size:1em;line-height:1.6;margin-bottom:1.25em;list-style-position:outside;font-family:inherit}
|
|
||||||
ul,ol,ul.no-bullet,ol.no-bullet{margin-left:1.5em}
|
|
||||||
ul li ul,ul li ol{margin-left:1.25em;margin-bottom:0;font-size:1em}
|
|
||||||
ul.square li ul,ul.circle li ul,ul.disc li ul{list-style:inherit}
|
|
||||||
ul.square{list-style-type:square}
|
|
||||||
ul.circle{list-style-type:circle}
|
|
||||||
ul.disc{list-style-type:disc}
|
|
||||||
ul.no-bullet{list-style:none}
|
|
||||||
ol li ul,ol li ol{margin-left:1.25em;margin-bottom:0}
|
|
||||||
dl dt{margin-bottom:.3125em;font-weight:bold}
|
|
||||||
dl dd{margin-bottom:1.25em}
|
|
||||||
abbr,acronym{text-transform:uppercase;font-size:90%;color:rgba(0,0,0,.8);border-bottom:1px dotted #ddd;cursor:help}
|
|
||||||
abbr{text-transform:none}
|
|
||||||
blockquote{margin:0 0 1.25em;padding:.5625em 1.25em 0 1.1875em;border-left:1px solid #ddd}
|
|
||||||
blockquote cite{display:block;font-size:.9375em;color:rgba(0,0,0,.6)}
|
|
||||||
blockquote cite:before{content:"\2014 \0020"}
|
|
||||||
blockquote cite a,blockquote cite a:visited{color:rgba(0,0,0,.6)}
|
|
||||||
blockquote,blockquote p{line-height:1.6;color:rgba(0,0,0,.85)}
|
|
||||||
@media only screen and (min-width:768px){h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2}
|
|
||||||
h1{font-size:2.75em}
|
|
||||||
h2{font-size:2.3125em}
|
|
||||||
h3,#toctitle,.sidebarblock>.content>.title{font-size:1.6875em}
|
|
||||||
h4{font-size:1.4375em}}table{background:#fff;margin-bottom:1.25em;border:solid 1px #dedede}
|
|
||||||
table thead,table tfoot{background:#f7f8f7;font-weight:bold}
|
|
||||||
table thead tr th,table thead tr td,table tfoot tr th,table tfoot tr td{padding:.5em .625em .625em;font-size:inherit;color:rgba(0,0,0,.8);text-align:left}
|
|
||||||
table tr th,table tr td{padding:.5625em .625em;font-size:inherit;color:rgba(0,0,0,.8)}
|
|
||||||
table tr.even,table tr.alt,table tr:nth-of-type(even){background:#f8f8f7}
|
|
||||||
table thead tr th,table tfoot tr th,table tbody tr td,table tr td,table tfoot tr td{display:table-cell;line-height:1.6}
|
|
||||||
h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2;word-spacing:-.05em}
|
|
||||||
h1 strong,h2 strong,h3 strong,#toctitle strong,.sidebarblock>.content>.title strong,h4 strong,h5 strong,h6 strong{font-weight:400}
|
|
||||||
.clearfix:before,.clearfix:after,.float-group:before,.float-group:after{content:" ";display:table}
|
|
||||||
.clearfix:after,.float-group:after{clear:both}
|
|
||||||
*:not(pre)>code{font-size:.9375em;font-style:normal!important;letter-spacing:0;padding:.1em .5ex;word-spacing:-.15em;background-color:#f7f7f8;-webkit-border-radius:4px;border-radius:4px;line-height:1.45;text-rendering:optimizeSpeed}
|
|
||||||
pre,pre>code{line-height:1.45;color:rgba(0,0,0,.9);font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;font-weight:400;text-rendering:optimizeSpeed}
|
|
||||||
.keyseq{color:rgba(51,51,51,.8)}
|
|
||||||
kbd{display:inline-block;color:rgba(0,0,0,.8);font-size:.75em;line-height:1.4;background-color:#f7f7f7;border:1px solid #ccc;-webkit-border-radius:3px;border-radius:3px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,.2),0 0 0 .1em white inset;box-shadow:0 1px 0 rgba(0,0,0,.2),0 0 0 .1em #fff inset;margin:-.15em .15em 0 .15em;padding:.2em .6em .2em .5em;vertical-align:middle;white-space:nowrap}
|
|
||||||
.keyseq kbd:first-child{margin-left:0}
|
|
||||||
.keyseq kbd:last-child{margin-right:0}
|
|
||||||
.menuseq,.menu{color:rgba(0,0,0,.8)}
|
|
||||||
b.button:before,b.button:after{position:relative;top:-1px;font-weight:400}
|
|
||||||
b.button:before{content:"[";padding:0 3px 0 2px}
|
|
||||||
b.button:after{content:"]";padding:0 2px 0 3px}
|
|
||||||
p a>code:hover{color:rgba(0,0,0,.9)}
|
|
||||||
#header,#content,#footnotes,#footer{width:100%;margin-left:auto;margin-right:auto;margin-top:0;margin-bottom:0;max-width:62.5em;*zoom:1;position:relative;padding-left:.9375em;padding-right:.9375em}
|
|
||||||
#header:before,#header:after,#content:before,#content:after,#footnotes:before,#footnotes:after,#footer:before,#footer:after{content:" ";display:table}
|
|
||||||
#header:after,#content:after,#footnotes:after,#footer:after{clear:both}
|
|
||||||
#content{margin-top:1.25em}
|
|
||||||
#content:before{content:none}
|
|
||||||
#header>h1:first-child{color:rgba(0,0,0,.85);margin-top:2.25rem;margin-bottom:0}
|
|
||||||
#header>h1:first-child+#toc{margin-top:8px;border-top:1px solid #ddddd8}
|
|
||||||
#header>h1:only-child,body.toc2 #header>h1:nth-last-child(2){border-bottom:1px solid #ddddd8;padding-bottom:8px}
|
|
||||||
#header .details{border-bottom:1px solid #ddddd8;line-height:1.45;padding-top:.25em;padding-bottom:.25em;padding-left:.25em;color:rgba(0,0,0,.6);display:-ms-flexbox;display:-webkit-flex;display:flex;-ms-flex-flow:row wrap;-webkit-flex-flow:row wrap;flex-flow:row wrap}
|
|
||||||
#header .details span:first-child{margin-left:-.125em}
|
|
||||||
#header .details span.email a{color:rgba(0,0,0,.85)}
|
|
||||||
#header .details br{display:none}
|
|
||||||
#header .details br+span:before{content:"\00a0\2013\00a0"}
|
|
||||||
#header .details br+span.author:before{content:"\00a0\22c5\00a0";color:rgba(0,0,0,.85)}
|
|
||||||
#header .details br+span#revremark:before{content:"\00a0|\00a0"}
|
|
||||||
#header #revnumber{text-transform:capitalize}
|
|
||||||
#header #revnumber:after{content:"\00a0"}
|
|
||||||
#content>h1:first-child:not([class]){color:rgba(0,0,0,.85);border-bottom:1px solid #ddddd8;padding-bottom:8px;margin-top:0;padding-top:1rem;margin-bottom:1.25rem}
|
|
||||||
#toc{border-bottom:1px solid #efefed;padding-bottom:.5em}
|
|
||||||
#toc>ul{margin-left:.125em}
|
|
||||||
#toc ul.sectlevel0>li>a{font-style:italic}
|
|
||||||
#toc ul.sectlevel0 ul.sectlevel1{margin:.5em 0}
|
|
||||||
#toc ul{font-family:"Open Sans","DejaVu Sans",sans-serif;list-style-type:none}
|
|
||||||
#toc a{text-decoration:none}
|
|
||||||
#toc a:active{text-decoration:underline}
|
|
||||||
#toctitle{color:#7a2518;font-size:1.2em}
|
|
||||||
@media only screen and (min-width:768px){#toctitle{font-size:1.375em}
|
|
||||||
body.toc2{padding-left:15em;padding-right:0}
|
|
||||||
#toc.toc2{margin-top:0!important;background-color:#f8f8f7;position:fixed;width:15em;left:0;top:0;border-right:1px solid #efefed;border-top-width:0!important;border-bottom-width:0!important;z-index:1000;padding:1.25em 1em;height:100%;overflow:auto}
|
|
||||||
#toc.toc2 #toctitle{margin-top:0;font-size:1.2em}
|
|
||||||
#toc.toc2>ul{font-size:.9em;margin-bottom:0}
|
|
||||||
#toc.toc2 ul ul{margin-left:0;padding-left:1em}
|
|
||||||
#toc.toc2 ul.sectlevel0 ul.sectlevel1{padding-left:0;margin-top:.5em;margin-bottom:.5em}
|
|
||||||
body.toc2.toc-right{padding-left:0;padding-right:15em}
|
|
||||||
body.toc2.toc-right #toc.toc2{border-right-width:0;border-left:1px solid #efefed;left:auto;right:0}}@media only screen and (min-width:1280px){body.toc2{padding-left:20em;padding-right:0}
|
|
||||||
#toc.toc2{width:20em}
|
|
||||||
#toc.toc2 #toctitle{font-size:1.375em}
|
|
||||||
#toc.toc2>ul{font-size:.95em}
|
|
||||||
#toc.toc2 ul ul{padding-left:1.25em}
|
|
||||||
body.toc2.toc-right{padding-left:0;padding-right:20em}}#content #toc{border-style:solid;border-width:1px;border-color:#e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;-webkit-border-radius:4px;border-radius:4px}
|
|
||||||
#content #toc>:first-child{margin-top:0}
|
|
||||||
#content #toc>:last-child{margin-bottom:0}
|
|
||||||
#footer{max-width:100%;background-color:rgba(0,0,0,.8);padding:1.25em}
|
|
||||||
#footer-text{color:rgba(255,255,255,.8);line-height:1.44}
|
|
||||||
.sect1{padding-bottom:.625em}
|
|
||||||
@media only screen and (min-width:768px){.sect1{padding-bottom:1.25em}}.sect1+.sect1{border-top:1px solid #efefed}
|
|
||||||
#content h1>a.anchor,h2>a.anchor,h3>a.anchor,#toctitle>a.anchor,.sidebarblock>.content>.title>a.anchor,h4>a.anchor,h5>a.anchor,h6>a.anchor{position:absolute;z-index:1001;width:1.5ex;margin-left:-1.5ex;display:block;text-decoration:none!important;visibility:hidden;text-align:center;font-weight:400}
|
|
||||||
#content h1>a.anchor:before,h2>a.anchor:before,h3>a.anchor:before,#toctitle>a.anchor:before,.sidebarblock>.content>.title>a.anchor:before,h4>a.anchor:before,h5>a.anchor:before,h6>a.anchor:before{content:"\00A7";font-size:.85em;display:block;padding-top:.1em}
|
|
||||||
#content h1:hover>a.anchor,#content h1>a.anchor:hover,h2:hover>a.anchor,h2>a.anchor:hover,h3:hover>a.anchor,#toctitle:hover>a.anchor,.sidebarblock>.content>.title:hover>a.anchor,h3>a.anchor:hover,#toctitle>a.anchor:hover,.sidebarblock>.content>.title>a.anchor:hover,h4:hover>a.anchor,h4>a.anchor:hover,h5:hover>a.anchor,h5>a.anchor:hover,h6:hover>a.anchor,h6>a.anchor:hover{visibility:visible}
|
|
||||||
#content h1>a.link,h2>a.link,h3>a.link,#toctitle>a.link,.sidebarblock>.content>.title>a.link,h4>a.link,h5>a.link,h6>a.link{color:#ba3925;text-decoration:none}
|
|
||||||
#content h1>a.link:hover,h2>a.link:hover,h3>a.link:hover,#toctitle>a.link:hover,.sidebarblock>.content>.title>a.link:hover,h4>a.link:hover,h5>a.link:hover,h6>a.link:hover{color:#a53221}
|
|
||||||
.audioblock,.imageblock,.literalblock,.listingblock,.stemblock,.videoblock{margin-bottom:1.25em}
|
|
||||||
.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{text-rendering:optimizeLegibility;text-align:left;font-family:"Noto Serif","DejaVu Serif",serif;font-size:1rem;font-style:italic}
|
|
||||||
table.tableblock>caption.title{white-space:nowrap;overflow:visible;max-width:0}
|
|
||||||
.paragraph.lead>p,#preamble>.sectionbody>.paragraph:first-of-type p{color:rgba(0,0,0,.85)}
|
|
||||||
table.tableblock #preamble>.sectionbody>.paragraph:first-of-type p{font-size:inherit}
|
|
||||||
.admonitionblock>table{border-collapse:separate;border:0;background:none;width:100%}
|
|
||||||
.admonitionblock>table td.icon{text-align:center;width:80px}
|
|
||||||
.admonitionblock>table td.icon img{max-width:none}
|
|
||||||
.admonitionblock>table td.icon .title{font-weight:bold;font-family:"Open Sans","DejaVu Sans",sans-serif;text-transform:uppercase}
|
|
||||||
.admonitionblock>table td.content{padding-left:1.125em;padding-right:1.25em;border-left:1px solid #ddddd8;color:rgba(0,0,0,.6)}
|
|
||||||
.admonitionblock>table td.content>:last-child>:last-child{margin-bottom:0}
|
|
||||||
.exampleblock>.content{border-style:solid;border-width:1px;border-color:#e6e6e6;margin-bottom:1.25em;padding:1.25em;background:#fff;-webkit-border-radius:4px;border-radius:4px}
|
|
||||||
.exampleblock>.content>:first-child{margin-top:0}
|
|
||||||
.exampleblock>.content>:last-child{margin-bottom:0}
|
|
||||||
.sidebarblock{border-style:solid;border-width:1px;border-color:#e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;-webkit-border-radius:4px;border-radius:4px}
|
|
||||||
.sidebarblock>:first-child{margin-top:0}
|
|
||||||
.sidebarblock>:last-child{margin-bottom:0}
|
|
||||||
.sidebarblock>.content>.title{color:#7a2518;margin-top:0;text-align:center}
|
|
||||||
.exampleblock>.content>:last-child>:last-child,.exampleblock>.content .olist>ol>li:last-child>:last-child,.exampleblock>.content .ulist>ul>li:last-child>:last-child,.exampleblock>.content .qlist>ol>li:last-child>:last-child,.sidebarblock>.content>:last-child>:last-child,.sidebarblock>.content .olist>ol>li:last-child>:last-child,.sidebarblock>.content .ulist>ul>li:last-child>:last-child,.sidebarblock>.content .qlist>ol>li:last-child>:last-child{margin-bottom:0}
|
|
||||||
.literalblock pre,.listingblock pre:not(.highlight),.listingblock pre[class="highlight"],.listingblock pre[class^="highlight "],.listingblock pre.CodeRay,.listingblock pre.prettyprint{background:#f7f7f8}
|
|
||||||
.sidebarblock .literalblock pre,.sidebarblock .listingblock pre:not(.highlight),.sidebarblock .listingblock pre[class="highlight"],.sidebarblock .listingblock pre[class^="highlight "],.sidebarblock .listingblock pre.CodeRay,.sidebarblock .listingblock pre.prettyprint{background:#f2f1f1}
|
|
||||||
.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{-webkit-border-radius:4px;border-radius:4px;word-wrap:break-word;padding:1em;font-size:.8125em}
|
|
||||||
.literalblock pre.nowrap,.literalblock pre[class].nowrap,.listingblock pre.nowrap,.listingblock pre[class].nowrap{overflow-x:auto;white-space:pre;word-wrap:normal}
|
|
||||||
@media only screen and (min-width:768px){.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{font-size:.90625em}}@media only screen and (min-width:1280px){.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{font-size:1em}}.literalblock.output pre{color:#f7f7f8;background-color:rgba(0,0,0,.9)}
|
|
||||||
.listingblock pre.highlightjs{padding:0}
|
|
||||||
.listingblock pre.highlightjs>code{padding:1em;-webkit-border-radius:4px;border-radius:4px}
|
|
||||||
.listingblock pre.prettyprint{border-width:0}
|
|
||||||
.listingblock>.content{position:relative}
|
|
||||||
.listingblock code[data-lang]:before{display:none;content:attr(data-lang);position:absolute;font-size:.75em;top:.425rem;right:.5rem;line-height:1;text-transform:uppercase;color:#999}
|
|
||||||
.listingblock:hover code[data-lang]:before{display:block}
|
|
||||||
.listingblock.terminal pre .command:before{content:attr(data-prompt);padding-right:.5em;color:#999}
|
|
||||||
.listingblock.terminal pre .command:not([data-prompt]):before{content:"$"}
|
|
||||||
table.pyhltable{border-collapse:separate;border:0;margin-bottom:0;background:none}
|
|
||||||
table.pyhltable td{vertical-align:top;padding-top:0;padding-bottom:0}
|
|
||||||
table.pyhltable td.code{padding-left:.75em;padding-right:0}
|
|
||||||
pre.pygments .lineno,table.pyhltable td:not(.code){color:#999;padding-left:0;padding-right:.5em;border-right:1px solid #ddddd8}
|
|
||||||
pre.pygments .lineno{display:inline-block;margin-right:.25em}
|
|
||||||
table.pyhltable .linenodiv{background:none!important;padding-right:0!important}
|
|
||||||
.quoteblock{margin:0 1em 1.25em 1.5em;display:table}
|
|
||||||
.quoteblock>.title{margin-left:-1.5em;margin-bottom:.75em}
|
|
||||||
.quoteblock blockquote,.quoteblock blockquote p{color:rgba(0,0,0,.85);font-size:1.15rem;line-height:1.75;word-spacing:.1em;letter-spacing:0;font-style:italic;text-align:justify}
|
|
||||||
.quoteblock blockquote{margin:0;padding:0;border:0}
|
|
||||||
.quoteblock blockquote:before{content:"\201c";float:left;font-size:2.75em;font-weight:bold;line-height:.6em;margin-left:-.6em;color:#7a2518;text-shadow:0 1px 2px rgba(0,0,0,.1)}
|
|
||||||
.quoteblock blockquote>.paragraph:last-child p{margin-bottom:0}
|
|
||||||
.quoteblock .attribution{margin-top:.5em;margin-right:.5ex;text-align:right}
|
|
||||||
.quoteblock .quoteblock{margin-left:0;margin-right:0;padding:.5em 0;border-left:3px solid rgba(0,0,0,.6)}
|
|
||||||
.quoteblock .quoteblock blockquote{padding:0 0 0 .75em}
|
|
||||||
.quoteblock .quoteblock blockquote:before{display:none}
|
|
||||||
.verseblock{margin:0 1em 1.25em 1em}
|
|
||||||
.verseblock pre{font-family:"Open Sans","DejaVu Sans",sans;font-size:1.15rem;color:rgba(0,0,0,.85);font-weight:300;text-rendering:optimizeLegibility}
|
|
||||||
.verseblock pre strong{font-weight:400}
|
|
||||||
.verseblock .attribution{margin-top:1.25rem;margin-left:.5ex}
|
|
||||||
.quoteblock .attribution,.verseblock .attribution{font-size:.9375em;line-height:1.45;font-style:italic}
|
|
||||||
.quoteblock .attribution br,.verseblock .attribution br{display:none}
|
|
||||||
.quoteblock .attribution cite,.verseblock .attribution cite{display:block;letter-spacing:-.05em;color:rgba(0,0,0,.6)}
|
|
||||||
.quoteblock.abstract{margin:0 0 1.25em 0;display:block}
|
|
||||||
.quoteblock.abstract blockquote,.quoteblock.abstract blockquote p{text-align:left;word-spacing:0}
|
|
||||||
.quoteblock.abstract blockquote:before,.quoteblock.abstract blockquote p:first-of-type:before{display:none}
|
|
||||||
table.tableblock{max-width:100%;border-collapse:separate}
|
|
||||||
table.tableblock td>.paragraph:last-child p>p:last-child,table.tableblock th>p:last-child,table.tableblock td>p:last-child{margin-bottom:0}
|
|
||||||
table.spread{width:100%}
|
|
||||||
table.tableblock,th.tableblock,td.tableblock{border:0 solid #dedede}
|
|
||||||
table.grid-all th.tableblock,table.grid-all td.tableblock{border-width:0 1px 1px 0}
|
|
||||||
table.grid-all tfoot>tr>th.tableblock,table.grid-all tfoot>tr>td.tableblock{border-width:1px 1px 0 0}
|
|
||||||
table.grid-cols th.tableblock,table.grid-cols td.tableblock{border-width:0 1px 0 0}
|
|
||||||
table.grid-all *>tr>.tableblock:last-child,table.grid-cols *>tr>.tableblock:last-child{border-right-width:0}
|
|
||||||
table.grid-rows th.tableblock,table.grid-rows td.tableblock{border-width:0 0 1px 0}
|
|
||||||
table.grid-all tbody>tr:last-child>th.tableblock,table.grid-all tbody>tr:last-child>td.tableblock,table.grid-all thead:last-child>tr>th.tableblock,table.grid-rows tbody>tr:last-child>th.tableblock,table.grid-rows tbody>tr:last-child>td.tableblock,table.grid-rows thead:last-child>tr>th.tableblock{border-bottom-width:0}
|
|
||||||
table.grid-rows tfoot>tr>th.tableblock,table.grid-rows tfoot>tr>td.tableblock{border-width:1px 0 0 0}
|
|
||||||
table.frame-all{border-width:1px}
|
|
||||||
table.frame-sides{border-width:0 1px}
|
|
||||||
table.frame-topbot{border-width:1px 0}
|
|
||||||
th.halign-left,td.halign-left{text-align:left}
|
|
||||||
th.halign-right,td.halign-right{text-align:right}
|
|
||||||
th.halign-center,td.halign-center{text-align:center}
|
|
||||||
th.valign-top,td.valign-top{vertical-align:top}
|
|
||||||
th.valign-bottom,td.valign-bottom{vertical-align:bottom}
|
|
||||||
th.valign-middle,td.valign-middle{vertical-align:middle}
|
|
||||||
table thead th,table tfoot th{font-weight:bold}
|
|
||||||
tbody tr th{display:table-cell;line-height:1.6;background:#f7f8f7}
|
|
||||||
tbody tr th,tbody tr th p,tfoot tr th,tfoot tr th p{color:rgba(0,0,0,.8);font-weight:bold}
|
|
||||||
p.tableblock>code:only-child{background:none;padding:0}
|
|
||||||
p.tableblock{font-size:1em}
|
|
||||||
td>div.verse{white-space:pre}
|
|
||||||
ol{margin-left:1.75em}
|
|
||||||
ul li ol{margin-left:1.5em}
|
|
||||||
dl dd{margin-left:1.125em}
|
|
||||||
dl dd:last-child,dl dd:last-child>:last-child{margin-bottom:0}
|
|
||||||
ol>li p,ul>li p,ul dd,ol dd,.olist .olist,.ulist .ulist,.ulist .olist,.olist .ulist{margin-bottom:.625em}
|
|
||||||
ul.unstyled,ol.unnumbered,ul.checklist,ul.none{list-style-type:none}
|
|
||||||
ul.unstyled,ol.unnumbered,ul.checklist{margin-left:.625em}
|
|
||||||
ul.checklist li>p:first-child>.fa-check-square-o:first-child,ul.checklist li>p:first-child>input[type="checkbox"]:first-child{margin-right:.25em}
|
|
||||||
ul.checklist li>p:first-child>input[type="checkbox"]:first-child{position:relative;top:1px}
|
|
||||||
ul.inline{margin:0 auto .625em auto;margin-left:-1.375em;margin-right:0;padding:0;list-style:none;overflow:hidden}
|
|
||||||
ul.inline>li{list-style:none;float:left;margin-left:1.375em;display:block}
|
|
||||||
ul.inline>li>*{display:block}
|
|
||||||
.unstyled dl dt{font-weight:400;font-style:normal}
|
|
||||||
ol.arabic{list-style-type:decimal}
|
|
||||||
ol.decimal{list-style-type:decimal-leading-zero}
|
|
||||||
ol.loweralpha{list-style-type:lower-alpha}
|
|
||||||
ol.upperalpha{list-style-type:upper-alpha}
|
|
||||||
ol.lowerroman{list-style-type:lower-roman}
|
|
||||||
ol.upperroman{list-style-type:upper-roman}
|
|
||||||
ol.lowergreek{list-style-type:lower-greek}
|
|
||||||
.hdlist>table,.colist>table{border:0;background:none}
|
|
||||||
.hdlist>table>tbody>tr,.colist>table>tbody>tr{background:none}
|
|
||||||
td.hdlist1{padding-right:.75em;font-weight:bold}
|
|
||||||
td.hdlist1,td.hdlist2{vertical-align:top}
|
|
||||||
.literalblock+.colist,.listingblock+.colist{margin-top:-.5em}
|
|
||||||
.colist>table tr>td:first-of-type{padding:0 .75em;line-height:1}
|
|
||||||
.colist>table tr>td:last-of-type{padding:.25em 0}
|
|
||||||
.thumb,.th{line-height:0;display:inline-block;border:solid 4px #fff;-webkit-box-shadow:0 0 0 1px #ddd;box-shadow:0 0 0 1px #ddd}
|
|
||||||
.imageblock.left,.imageblock[style*="float: left"]{margin:.25em .625em 1.25em 0}
|
|
||||||
.imageblock.right,.imageblock[style*="float: right"]{margin:.25em 0 1.25em .625em}
|
|
||||||
.imageblock>.title{margin-bottom:0}
|
|
||||||
.imageblock.thumb,.imageblock.th{border-width:6px}
|
|
||||||
.imageblock.thumb>.title,.imageblock.th>.title{padding:0 .125em}
|
|
||||||
.image.left,.image.right{margin-top:.25em;margin-bottom:.25em;display:inline-block;line-height:0}
|
|
||||||
.image.left{margin-right:.625em}
|
|
||||||
.image.right{margin-left:.625em}
|
|
||||||
a.image{text-decoration:none}
|
|
||||||
span.footnote,span.footnoteref{vertical-align:super;font-size:.875em}
|
|
||||||
span.footnote a,span.footnoteref a{text-decoration:none}
|
|
||||||
span.footnote a:active,span.footnoteref a:active{text-decoration:underline}
|
|
||||||
#footnotes{padding-top:.75em;padding-bottom:.75em;margin-bottom:.625em}
|
|
||||||
#footnotes hr{width:20%;min-width:6.25em;margin:-.25em 0 .75em 0;border-width:1px 0 0 0}
|
|
||||||
#footnotes .footnote{padding:0 .375em;line-height:1.3;font-size:.875em;margin-left:1.2em;text-indent:-1.2em;margin-bottom:.2em}
|
|
||||||
#footnotes .footnote a:first-of-type{font-weight:bold;text-decoration:none}
|
|
||||||
#footnotes .footnote:last-of-type{margin-bottom:0}
|
|
||||||
#content #footnotes{margin-top:-.625em;margin-bottom:0;padding:.75em 0}
|
|
||||||
.gist .file-data>table{border:0;background:#fff;width:100%;margin-bottom:0}
|
|
||||||
.gist .file-data>table td.line-data{width:99%}
|
|
||||||
div.unbreakable{page-break-inside:avoid}
|
|
||||||
.big{font-size:larger}
|
|
||||||
.small{font-size:smaller}
|
|
||||||
.underline{text-decoration:underline}
|
|
||||||
.overline{text-decoration:overline}
|
|
||||||
.line-through{text-decoration:line-through}
|
|
||||||
.aqua{color:#00bfbf}
|
|
||||||
.aqua-background{background-color:#00fafa}
|
|
||||||
.black{color:#000}
|
|
||||||
.black-background{background-color:#000}
|
|
||||||
.blue{color:#0000bf}
|
|
||||||
.blue-background{background-color:#0000fa}
|
|
||||||
.fuchsia{color:#bf00bf}
|
|
||||||
.fuchsia-background{background-color:#fa00fa}
|
|
||||||
.gray{color:#606060}
|
|
||||||
.gray-background{background-color:#7d7d7d}
|
|
||||||
.green{color:#006000}
|
|
||||||
.green-background{background-color:#007d00}
|
|
||||||
.lime{color:#00bf00}
|
|
||||||
.lime-background{background-color:#00fa00}
|
|
||||||
.maroon{color:#600000}
|
|
||||||
.maroon-background{background-color:#7d0000}
|
|
||||||
.navy{color:#000060}
|
|
||||||
.navy-background{background-color:#00007d}
|
|
||||||
.olive{color:#606000}
|
|
||||||
.olive-background{background-color:#7d7d00}
|
|
||||||
.purple{color:#600060}
|
|
||||||
.purple-background{background-color:#7d007d}
|
|
||||||
.red{color:#bf0000}
|
|
||||||
.red-background{background-color:#fa0000}
|
|
||||||
.silver{color:#909090}
|
|
||||||
.silver-background{background-color:#bcbcbc}
|
|
||||||
.teal{color:#006060}
|
|
||||||
.teal-background{background-color:#007d7d}
|
|
||||||
.white{color:#bfbfbf}
|
|
||||||
.white-background{background-color:#fafafa}
|
|
||||||
.yellow{color:#bfbf00}
|
|
||||||
.yellow-background{background-color:#fafa00}
|
|
||||||
span.icon>.fa{cursor:default}
|
|
||||||
.admonitionblock td.icon [class^="fa icon-"]{font-size:2.5em;text-shadow:1px 1px 2px rgba(0,0,0,.5);cursor:default}
|
|
||||||
.admonitionblock td.icon .icon-note:before{content:"\f05a";color:#19407c}
|
|
||||||
.admonitionblock td.icon .icon-tip:before{content:"\f0eb";text-shadow:1px 1px 2px rgba(155,155,0,.8);color:#111}
|
|
||||||
.admonitionblock td.icon .icon-warning:before{content:"\f071";color:#bf6900}
|
|
||||||
.admonitionblock td.icon .icon-caution:before{content:"\f06d";color:#bf3400}
|
|
||||||
.admonitionblock td.icon .icon-important:before{content:"\f06a";color:#bf0000}
|
|
||||||
.conum[data-value]{display:inline-block;color:#fff!important;background-color:rgba(0,0,0,.8);-webkit-border-radius:100px;border-radius:100px;text-align:center;font-size:.75em;width:1.67em;height:1.67em;line-height:1.67em;font-family:"Open Sans","DejaVu Sans",sans-serif;font-style:normal;font-weight:bold}
|
|
||||||
.conum[data-value] *{color:#fff!important}
|
|
||||||
.conum[data-value]+b{display:none}
|
|
||||||
.conum[data-value]:after{content:attr(data-value)}
|
|
||||||
pre .conum[data-value]{position:relative;top:-.125em}
|
|
||||||
b.conum *{color:inherit!important}
|
|
||||||
.conum:not([data-value]):empty{display:none}
|
|
||||||
h1,h2{letter-spacing:-.01em}
|
|
||||||
dt,th.tableblock,td.content{text-rendering:optimizeLegibility}
|
|
||||||
p,td.content{letter-spacing:-.01em}
|
|
||||||
p strong,td.content strong{letter-spacing:-.005em}
|
|
||||||
p,blockquote,dt,td.content{font-size:1.0625rem}
|
|
||||||
p{margin-bottom:1.25rem}
|
|
||||||
.sidebarblock p,.sidebarblock dt,.sidebarblock td.content,p.tableblock{font-size:1em}
|
|
||||||
.exampleblock>.content{background-color:#fffef7;border-color:#e0e0dc;-webkit-box-shadow:0 1px 4px #e0e0dc;box-shadow:0 1px 4px #e0e0dc}
|
|
||||||
.print-only{display:none!important}
|
|
||||||
@media print{@page{margin:1.25cm .75cm}
|
|
||||||
*{-webkit-box-shadow:none!important;box-shadow:none!important;text-shadow:none!important}
|
|
||||||
a{color:inherit!important;text-decoration:underline!important}
|
|
||||||
a.bare,a[href^="#"],a[href^="mailto:"]{text-decoration:none!important}
|
|
||||||
a[href^="http:"]:not(.bare):after,a[href^="https:"]:not(.bare):after{content:"(" attr(href) ")";display:inline-block;font-size:.875em;padding-left:.25em}
|
|
||||||
abbr[title]:after{content:" (" attr(title) ")"}
|
|
||||||
pre,blockquote,tr,img{page-break-inside:avoid}
|
|
||||||
thead{display:table-header-group}
|
|
||||||
img{max-width:100%!important}
|
|
||||||
p,blockquote,dt,td.content{font-size:1em;orphans:3;widows:3}
|
|
||||||
h2,h3,#toctitle,.sidebarblock>.content>.title{page-break-after:avoid}
|
|
||||||
#toc,.sidebarblock,.exampleblock>.content{background:none!important}
|
|
||||||
#toc{border-bottom:1px solid #ddddd8!important;padding-bottom:0!important}
|
|
||||||
.sect1{padding-bottom:0!important}
|
|
||||||
.sect1+.sect1{border:0!important}
|
|
||||||
#header>h1:first-child{margin-top:1.25rem}
|
|
||||||
body.book #header{text-align:center}
|
|
||||||
body.book #header>h1:first-child{border:0!important;margin:2.5em 0 1em 0}
|
|
||||||
body.book #header .details{border:0!important;display:block;padding:0!important}
|
|
||||||
body.book #header .details span:first-child{margin-left:0!important}
|
|
||||||
body.book #header .details br{display:block}
|
|
||||||
body.book #header .details br+span:before{content:none!important}
|
|
||||||
body.book #toc{border:0!important;text-align:left!important;padding:0!important;margin:0!important}
|
|
||||||
body.book #toc,body.book #preamble,body.book h1.sect0,body.book .sect1>h2{page-break-before:always}
|
|
||||||
.listingblock code[data-lang]:before{display:block}
|
|
||||||
#footer{background:none!important;padding:0 .9375em}
|
|
||||||
#footer-text{color:rgba(0,0,0,.6)!important;font-size:.9em}
|
|
||||||
.hide-on-print{display:none!important}
|
|
||||||
.print-only{display:block!important}
|
|
||||||
.hide-for-print{display:none!important}
|
|
||||||
.show-for-print{display:inherit!important}}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body class="article">
|
|
||||||
<div id="header">
|
|
||||||
<h1>Raspberry Pi camera/LED demo</h1>
|
|
||||||
</div>
|
|
||||||
<div id="content">
|
|
||||||
<div class="sect1">
|
|
||||||
<h2 id="_overview">Overview</h2>
|
|
||||||
<div class="sectionbody">
|
|
||||||
<div class="paragraph">
|
|
||||||
<p>The <a href="/">demo</a> consists of web app providing access to a webcam and a LED attached to a RaspberryPi.
|
|
||||||
The device is assumed to have a limited bandwidth towards the server hosting the web app.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="sect1">
|
|
||||||
<h2 id="_objective">Objective</h2>
|
|
||||||
<div class="sectionbody">
|
|
||||||
<div class="paragraph">
|
|
||||||
<p>The demo shows how to use websockets to communicate bidirectionally with an embedded device using standard protocols.</p>
|
|
||||||
</div>
|
|
||||||
<div class="paragraph">
|
|
||||||
<p>It also shows that it’s possible to use Smart.c to develop also the cloud endpoint and expose WebSocket and RESTful APIs
|
|
||||||
easy to integreate with modern web stacks.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="sect1">
|
|
||||||
<h2 id="_how_it_works">How it works</h2>
|
|
||||||
<div class="sectionbody">
|
|
||||||
<div class="imageblock">
|
|
||||||
<div class="content">
|
|
||||||
<img src="docs/arch.png" alt="arch">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="paragraph">
|
|
||||||
<p>There are two components, once with runs on the device (<code>device_side</code>) and one that runs on a stronger machine
|
|
||||||
and with more bandwidth (<code>cloud_side</code>).</p>
|
|
||||||
</div>
|
|
||||||
<div class="paragraph">
|
|
||||||
<p>The device app connects to the cloud app via websocket and sends a new jpeg frame as fast as the underlying <code>raspistill</code> camera
|
|
||||||
grabbing application can handle. The device automatically attempts reconnecting.</p>
|
|
||||||
</div>
|
|
||||||
<div class="paragraph">
|
|
||||||
<p>The cloud side serves the webapp static pages and serves an MPJEG image on <code>/mpjg</code>.
|
|
||||||
The MPJEG image handler blocks all the clients until a JPEG frame arrives via websocket
|
|
||||||
and then every client will receive a copy of the frame.</p>
|
|
||||||
</div>
|
|
||||||
<div class="paragraph">
|
|
||||||
<p>The web app can turn on and off the LED via a RESTful api accessible via the <code>/api</code> handler.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="sect1">
|
|
||||||
<h2 id="_installation">Installation</h2>
|
|
||||||
<div class="sectionbody">
|
|
||||||
<div class="sect2">
|
|
||||||
<h3 id="_server_side">Server side</h3>
|
|
||||||
<div class="listingblock">
|
|
||||||
<div class="content">
|
|
||||||
<pre>git clone https://github.com/cesanta/mongoose
|
|
||||||
cd mongoose/examples/web_demo
|
|
||||||
make cloud_side && ./cloud_side 0.0.0.0:8080</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="sect2">
|
|
||||||
<h3 id="_raspberry_pi">Raspberry Pi</h3>
|
|
||||||
<div class="paragraph">
|
|
||||||
<p>The instructions provided here are tailored for the Raspbian distribution.</p>
|
|
||||||
</div>
|
|
||||||
<div class="sect3">
|
|
||||||
<h4 id="_dependencies">Dependencies</h4>
|
|
||||||
<div class="dlist">
|
|
||||||
<dl>
|
|
||||||
<dt class="hdlist1">jpegoptim</dt>
|
|
||||||
<dd>
|
|
||||||
<p>apt-get install jpegoptim</p>
|
|
||||||
</dd>
|
|
||||||
<dt class="hdlist1">camera</dt>
|
|
||||||
<dd>
|
|
||||||
<p>run raspi-config and enable camera</p>
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="sect3">
|
|
||||||
<h4 id="_led">LED</h4>
|
|
||||||
<div class="paragraph">
|
|
||||||
<p>In order to access the led on your <a href="http://www.qdh.org.uk/wordpress/?page_id=15">HotPi</a>
|
|
||||||
board you need to export the gpio pins:</p>
|
|
||||||
</div>
|
|
||||||
<div class="listingblock">
|
|
||||||
<div class="content">
|
|
||||||
<pre>for i in 22 23 24; do
|
|
||||||
echo $i >/sys/class/gpio/export
|
|
||||||
echo out >/sys/class/gpio/gpio$i/direction
|
|
||||||
chgrp pi /sys/class/gpio/gpio$i/value
|
|
||||||
done</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="sect3">
|
|
||||||
<h4 id="_build_and_run">Build and run</h4>
|
|
||||||
<div class="listingblock">
|
|
||||||
<div class="content">
|
|
||||||
<pre>git clone https://github.com/cesanta/mongoose
|
|
||||||
cd mongoose/examples/web_demo
|
|
||||||
make device_side && ./device_side yourserver:8080</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="footer">
|
|
||||||
<div id="footer-text">
|
|
||||||
Last updated 2014-11-03 21:30:06 GMT
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1 +0,0 @@
|
|||||||
../../docs/arch.png
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,108 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<!-- Required meta tags-->
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no, minimal-ui">
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="framework7.min.css">
|
|
||||||
|
|
||||||
<title>Smart.c mjpg example</title>
|
|
||||||
<style type="text/css">
|
|
||||||
.image img {
|
|
||||||
display: block;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<!-- Status bar overlay for full screen mode (PhoneGap) -->
|
|
||||||
<div class="statusbar-overlay"></div>
|
|
||||||
|
|
||||||
<!-- Views -->
|
|
||||||
<div class="views">
|
|
||||||
<!-- Your main view, should have "view-main" class -->
|
|
||||||
<div class="view view-main">
|
|
||||||
<!-- Top Navbar-->
|
|
||||||
<div class="navbar">
|
|
||||||
<div class="navbar-inner">
|
|
||||||
<!-- We need cool sliding animation on title element, so we have additional "sliding" class -->
|
|
||||||
<div class="left">
|
|
||||||
<a href="docs/docs/doc.html" class="link" onclick="location='docs/docs/doc.html'">
|
|
||||||
<span>About</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="center sliding">Remote Camera</div>
|
|
||||||
<div class="right sliding">
|
|
||||||
<a href="https://github.com/cesanta/mongoose/tree/master/examples/raspberry_pi_mjpeg_led" class="link" onclick="location='https://github.com/cesanta/mongoose/tree/master/examples/raspberry_pi_mjpeg_led'">
|
|
||||||
<span>Github</span>
|
|
||||||
<i class="icon icon-next"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Pages container, because we use fixed-through navbar and toolbar, it has additional appropriate classes-->
|
|
||||||
<div class="pages navbar-through toolbar-through">
|
|
||||||
<!-- Page, "data-page" contains page name -->
|
|
||||||
<div data-page="index" class="page">
|
|
||||||
<!-- Scrollable page content -->
|
|
||||||
<div class="page-content">
|
|
||||||
<div class="content-block-title">Camera View</div>
|
|
||||||
<div class="content-block image">
|
|
||||||
<img src="/mjpg">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="content-block-title">Device Control</div>
|
|
||||||
<div class="list-block">
|
|
||||||
|
|
||||||
<form action="/api" method="GET" enctype="application/json"
|
|
||||||
id="form-control">
|
|
||||||
<ul>
|
|
||||||
|
|
||||||
<!-- Switch (Checkbox) -->
|
|
||||||
<li>
|
|
||||||
<div class="item-content">
|
|
||||||
<div class="item-inner">
|
|
||||||
<div class="item-title label">LED on/off</div>
|
|
||||||
<div class="item-input">
|
|
||||||
<label class="label-switch">
|
|
||||||
<input type="checkbox" name="onoff">
|
|
||||||
<div class="checkbox"></div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script type="text/javascript" src="framework7.min.js"></script>
|
|
||||||
<script type="text/javascript">
|
|
||||||
var myApp = new Framework7({
|
|
||||||
pushState: true,
|
|
||||||
swipePanel: 'left',
|
|
||||||
// ... other parameters
|
|
||||||
});
|
|
||||||
Dom7(document).on('change', '#form-control', function(ev) {
|
|
||||||
var data = myApp.formToJSON('#form-control');
|
|
||||||
var json = JSON.stringify(data);
|
|
||||||
Dom7.ajax({
|
|
||||||
url: '/api',
|
|
||||||
method: 'POST',
|
|
||||||
contentType: 'application/json',
|
|
||||||
data: json
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
758
mongoose.c
758
mongoose.c
@ -610,603 +610,6 @@ double cs_time() {
|
|||||||
return now;
|
return now;
|
||||||
}
|
}
|
||||||
#ifdef MG_MODULE_LINES
|
#ifdef MG_MODULE_LINES
|
||||||
#line 1 "./src/../deps/frozen/frozen.c"
|
|
||||||
#endif
|
|
||||||
/*
|
|
||||||
* Copyright (c) 2004-2013 Sergey Lyubka <valenok@gmail.com>
|
|
||||||
* Copyright (c) 2013 Cesanta Software Limited
|
|
||||||
* All rights reserved
|
|
||||||
*
|
|
||||||
* This library is dual-licensed: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License version 2 as
|
|
||||||
* published by the Free Software Foundation. For the terms of this
|
|
||||||
* license, see <http: *www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
* You are free to use this library under the terms of the GNU General
|
|
||||||
* Public License, but WITHOUT ANY WARRANTY; without even the implied
|
|
||||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* Alternatively, you can license this library under a commercial
|
|
||||||
* license, as set out in <http://cesanta.com/products.html>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef _CRT_SECURE_NO_WARNINGS
|
|
||||||
#define _CRT_SECURE_NO_WARNINGS /* Disable deprecation warning in VS2005+ */
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <stdarg.h>
|
|
||||||
/* Amalgamated: #include "frozen.h" */
|
|
||||||
|
|
||||||
#ifdef _WIN32
|
|
||||||
#define snprintf _snprintf
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef FROZEN_REALLOC
|
|
||||||
#define FROZEN_REALLOC realloc
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef FROZEN_FREE
|
|
||||||
#define FROZEN_FREE free
|
|
||||||
#endif
|
|
||||||
|
|
||||||
struct frozen {
|
|
||||||
const char *end;
|
|
||||||
const char *cur;
|
|
||||||
struct json_token *tokens;
|
|
||||||
int max_tokens;
|
|
||||||
int num_tokens;
|
|
||||||
int do_realloc;
|
|
||||||
};
|
|
||||||
|
|
||||||
static int parse_object(struct frozen *f);
|
|
||||||
static int parse_value(struct frozen *f);
|
|
||||||
|
|
||||||
#define EXPECT(cond, err_code) \
|
|
||||||
do { \
|
|
||||||
if (!(cond)) return (err_code); \
|
|
||||||
} while (0)
|
|
||||||
#define TRY(expr) \
|
|
||||||
do { \
|
|
||||||
int _n = expr; \
|
|
||||||
if (_n < 0) return _n; \
|
|
||||||
} while (0)
|
|
||||||
#define END_OF_STRING (-1)
|
|
||||||
|
|
||||||
static int left(const struct frozen *f) {
|
|
||||||
return f->end - f->cur;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int is_space(int ch) {
|
|
||||||
return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
static void skip_whitespaces(struct frozen *f) {
|
|
||||||
while (f->cur < f->end && is_space(*f->cur)) f->cur++;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int cur(struct frozen *f) {
|
|
||||||
skip_whitespaces(f);
|
|
||||||
return f->cur >= f->end ? END_OF_STRING : *(unsigned char *) f->cur;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int test_and_skip(struct frozen *f, int expected) {
|
|
||||||
int ch = cur(f);
|
|
||||||
if (ch == expected) {
|
|
||||||
f->cur++;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return ch == END_OF_STRING ? JSON_STRING_INCOMPLETE : JSON_STRING_INVALID;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int test_no_skip(struct frozen *f, int expected) {
|
|
||||||
int ch = cur(f);
|
|
||||||
if (ch == expected) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return ch == END_OF_STRING ? JSON_STRING_INCOMPLETE : JSON_STRING_INVALID;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int is_alpha(int ch) {
|
|
||||||
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z');
|
|
||||||
}
|
|
||||||
|
|
||||||
static int is_digit(int ch) {
|
|
||||||
return ch >= '0' && ch <= '9';
|
|
||||||
}
|
|
||||||
|
|
||||||
static int is_hex_digit(int ch) {
|
|
||||||
return is_digit(ch) || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F');
|
|
||||||
}
|
|
||||||
|
|
||||||
static int get_escape_len(const char *s, int len) {
|
|
||||||
switch (*s) {
|
|
||||||
case 'u':
|
|
||||||
return len < 6 ? JSON_STRING_INCOMPLETE
|
|
||||||
: is_hex_digit(s[1]) && is_hex_digit(s[2]) &&
|
|
||||||
is_hex_digit(s[3]) && is_hex_digit(s[4])
|
|
||||||
? 5
|
|
||||||
: JSON_STRING_INVALID;
|
|
||||||
case '"':
|
|
||||||
case '\\':
|
|
||||||
case '/':
|
|
||||||
case 'b':
|
|
||||||
case 'f':
|
|
||||||
case 'n':
|
|
||||||
case 'r':
|
|
||||||
case 't':
|
|
||||||
return len < 2 ? JSON_STRING_INCOMPLETE : 1;
|
|
||||||
default:
|
|
||||||
return JSON_STRING_INVALID;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static int capture_ptr(struct frozen *f, const char *ptr, enum json_type type) {
|
|
||||||
if (f->do_realloc && f->num_tokens >= f->max_tokens) {
|
|
||||||
int new_size = f->max_tokens == 0 ? 100 : f->max_tokens * 2;
|
|
||||||
void *p = FROZEN_REALLOC(f->tokens, new_size * sizeof(f->tokens[0]));
|
|
||||||
if (p == NULL) return JSON_TOKEN_ARRAY_TOO_SMALL;
|
|
||||||
f->max_tokens = new_size;
|
|
||||||
f->tokens = (struct json_token *) p;
|
|
||||||
}
|
|
||||||
if (f->tokens == NULL || f->max_tokens == 0) return 0;
|
|
||||||
if (f->num_tokens >= f->max_tokens) return JSON_TOKEN_ARRAY_TOO_SMALL;
|
|
||||||
f->tokens[f->num_tokens].ptr = ptr;
|
|
||||||
f->tokens[f->num_tokens].type = type;
|
|
||||||
f->num_tokens++;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int capture_len(struct frozen *f, int token_index, const char *ptr) {
|
|
||||||
if (f->tokens == 0 || f->max_tokens == 0) return 0;
|
|
||||||
EXPECT(token_index >= 0 && token_index < f->max_tokens, JSON_STRING_INVALID);
|
|
||||||
f->tokens[token_index].len = ptr - f->tokens[token_index].ptr;
|
|
||||||
f->tokens[token_index].num_desc = (f->num_tokens - 1) - token_index;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* identifier = letter { letter | digit | '_' } */
|
|
||||||
static int parse_identifier(struct frozen *f) {
|
|
||||||
EXPECT(is_alpha(cur(f)), JSON_STRING_INVALID);
|
|
||||||
TRY(capture_ptr(f, f->cur, JSON_TYPE_STRING));
|
|
||||||
while (f->cur < f->end &&
|
|
||||||
(*f->cur == '_' || is_alpha(*f->cur) || is_digit(*f->cur))) {
|
|
||||||
f->cur++;
|
|
||||||
}
|
|
||||||
capture_len(f, f->num_tokens - 1, f->cur);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int get_utf8_char_len(unsigned char ch) {
|
|
||||||
if ((ch & 0x80) == 0) return 1;
|
|
||||||
switch (ch & 0xf0) {
|
|
||||||
case 0xf0:
|
|
||||||
return 4;
|
|
||||||
case 0xe0:
|
|
||||||
return 3;
|
|
||||||
default:
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* string = '"' { quoted_printable_chars } '"' */
|
|
||||||
static int parse_string(struct frozen *f) {
|
|
||||||
int n, ch = 0, len = 0;
|
|
||||||
TRY(test_and_skip(f, '"'));
|
|
||||||
TRY(capture_ptr(f, f->cur, JSON_TYPE_STRING));
|
|
||||||
for (; f->cur < f->end; f->cur += len) {
|
|
||||||
ch = *(unsigned char *) f->cur;
|
|
||||||
len = get_utf8_char_len((unsigned char) ch);
|
|
||||||
EXPECT(ch >= 32 && len > 0, JSON_STRING_INVALID); /* No control chars */
|
|
||||||
EXPECT(len < left(f), JSON_STRING_INCOMPLETE);
|
|
||||||
if (ch == '\\') {
|
|
||||||
EXPECT((n = get_escape_len(f->cur + 1, left(f))) > 0, n);
|
|
||||||
len += n;
|
|
||||||
} else if (ch == '"') {
|
|
||||||
capture_len(f, f->num_tokens - 1, f->cur);
|
|
||||||
f->cur++;
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return ch == '"' ? 0 : JSON_STRING_INCOMPLETE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* number = [ '-' ] digit+ [ '.' digit+ ] [ ['e'|'E'] ['+'|'-'] digit+ ] */
|
|
||||||
static int parse_number(struct frozen *f) {
|
|
||||||
int ch = cur(f);
|
|
||||||
TRY(capture_ptr(f, f->cur, JSON_TYPE_NUMBER));
|
|
||||||
if (ch == '-') f->cur++;
|
|
||||||
EXPECT(f->cur < f->end, JSON_STRING_INCOMPLETE);
|
|
||||||
EXPECT(is_digit(f->cur[0]), JSON_STRING_INVALID);
|
|
||||||
while (f->cur < f->end && is_digit(f->cur[0])) f->cur++;
|
|
||||||
if (f->cur < f->end && f->cur[0] == '.') {
|
|
||||||
f->cur++;
|
|
||||||
EXPECT(f->cur < f->end, JSON_STRING_INCOMPLETE);
|
|
||||||
EXPECT(is_digit(f->cur[0]), JSON_STRING_INVALID);
|
|
||||||
while (f->cur < f->end && is_digit(f->cur[0])) f->cur++;
|
|
||||||
}
|
|
||||||
if (f->cur < f->end && (f->cur[0] == 'e' || f->cur[0] == 'E')) {
|
|
||||||
f->cur++;
|
|
||||||
EXPECT(f->cur < f->end, JSON_STRING_INCOMPLETE);
|
|
||||||
if ((f->cur[0] == '+' || f->cur[0] == '-')) f->cur++;
|
|
||||||
EXPECT(f->cur < f->end, JSON_STRING_INCOMPLETE);
|
|
||||||
EXPECT(is_digit(f->cur[0]), JSON_STRING_INVALID);
|
|
||||||
while (f->cur < f->end && is_digit(f->cur[0])) f->cur++;
|
|
||||||
}
|
|
||||||
capture_len(f, f->num_tokens - 1, f->cur);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* array = '[' [ value { ',' value } ] ']' */
|
|
||||||
static int parse_array(struct frozen *f) {
|
|
||||||
int ind;
|
|
||||||
TRY(test_and_skip(f, '['));
|
|
||||||
TRY(capture_ptr(f, f->cur - 1, JSON_TYPE_ARRAY));
|
|
||||||
ind = f->num_tokens - 1;
|
|
||||||
while (cur(f) != ']') {
|
|
||||||
TRY(parse_value(f));
|
|
||||||
if (cur(f) == ',') f->cur++;
|
|
||||||
}
|
|
||||||
TRY(test_and_skip(f, ']'));
|
|
||||||
capture_len(f, ind, f->cur);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int compare(const char *s, const char *str, int len) {
|
|
||||||
int i = 0;
|
|
||||||
while (i < len && s[i] == str[i]) i++;
|
|
||||||
return i == len ? 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int expect(struct frozen *f, const char *s, int len, enum json_type t) {
|
|
||||||
int i, n = left(f);
|
|
||||||
|
|
||||||
TRY(capture_ptr(f, f->cur, t));
|
|
||||||
for (i = 0; i < len; i++) {
|
|
||||||
if (i >= n) return JSON_STRING_INCOMPLETE;
|
|
||||||
if (f->cur[i] != s[i]) return JSON_STRING_INVALID;
|
|
||||||
}
|
|
||||||
f->cur += len;
|
|
||||||
TRY(capture_len(f, f->num_tokens - 1, f->cur));
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* value = 'null' | 'true' | 'false' | number | string | array | object */
|
|
||||||
static int parse_value(struct frozen *f) {
|
|
||||||
int ch = cur(f);
|
|
||||||
|
|
||||||
switch (ch) {
|
|
||||||
case '"':
|
|
||||||
TRY(parse_string(f));
|
|
||||||
break;
|
|
||||||
case '{':
|
|
||||||
TRY(parse_object(f));
|
|
||||||
break;
|
|
||||||
case '[':
|
|
||||||
TRY(parse_array(f));
|
|
||||||
break;
|
|
||||||
case 'n':
|
|
||||||
TRY(expect(f, "null", 4, JSON_TYPE_NULL));
|
|
||||||
break;
|
|
||||||
case 't':
|
|
||||||
TRY(expect(f, "true", 4, JSON_TYPE_TRUE));
|
|
||||||
break;
|
|
||||||
case 'f':
|
|
||||||
TRY(expect(f, "false", 5, JSON_TYPE_FALSE));
|
|
||||||
break;
|
|
||||||
case '-':
|
|
||||||
case '0':
|
|
||||||
case '1':
|
|
||||||
case '2':
|
|
||||||
case '3':
|
|
||||||
case '4':
|
|
||||||
case '5':
|
|
||||||
case '6':
|
|
||||||
case '7':
|
|
||||||
case '8':
|
|
||||||
case '9':
|
|
||||||
TRY(parse_number(f));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return ch == END_OF_STRING ? JSON_STRING_INCOMPLETE : JSON_STRING_INVALID;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* key = identifier | string */
|
|
||||||
static int parse_key(struct frozen *f) {
|
|
||||||
int ch = cur(f);
|
|
||||||
#if 0
|
|
||||||
printf("%s 1 [%.*s]\n", __func__, (int) (f->end - f->cur), f->cur);
|
|
||||||
#endif
|
|
||||||
if (is_alpha(ch)) {
|
|
||||||
TRY(parse_identifier(f));
|
|
||||||
} else if (ch == '"') {
|
|
||||||
TRY(parse_string(f));
|
|
||||||
} else {
|
|
||||||
return ch == END_OF_STRING ? JSON_STRING_INCOMPLETE : JSON_STRING_INVALID;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* pair = key ':' value */
|
|
||||||
static int parse_pair(struct frozen *f) {
|
|
||||||
TRY(parse_key(f));
|
|
||||||
TRY(test_and_skip(f, ':'));
|
|
||||||
TRY(parse_value(f));
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* object = '{' pair { ',' pair } '}' */
|
|
||||||
static int parse_object(struct frozen *f) {
|
|
||||||
int ind;
|
|
||||||
TRY(test_and_skip(f, '{'));
|
|
||||||
TRY(capture_ptr(f, f->cur - 1, JSON_TYPE_OBJECT));
|
|
||||||
ind = f->num_tokens - 1;
|
|
||||||
while (cur(f) != '}') {
|
|
||||||
TRY(parse_pair(f));
|
|
||||||
if (cur(f) == ',') f->cur++;
|
|
||||||
}
|
|
||||||
TRY(test_and_skip(f, '}'));
|
|
||||||
capture_len(f, ind, f->cur);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int doit(struct frozen *f) {
|
|
||||||
int ret = 0;
|
|
||||||
|
|
||||||
if (f->cur == 0 || f->end < f->cur) return JSON_STRING_INVALID;
|
|
||||||
if (f->end == f->cur) return JSON_STRING_INCOMPLETE;
|
|
||||||
|
|
||||||
if (0 == (ret = test_no_skip(f, '{'))) {
|
|
||||||
TRY(parse_object(f));
|
|
||||||
} else if (0 == (ret = test_no_skip(f, '['))) {
|
|
||||||
TRY(parse_array(f));
|
|
||||||
} else {
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
TRY(capture_ptr(f, f->cur, JSON_TYPE_EOF));
|
|
||||||
capture_len(f, f->num_tokens, f->cur);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* json = object */
|
|
||||||
int parse_json(const char *s, int s_len, struct json_token *arr, int arr_len) {
|
|
||||||
struct frozen frozen;
|
|
||||||
|
|
||||||
memset(&frozen, 0, sizeof(frozen));
|
|
||||||
frozen.end = s + s_len;
|
|
||||||
frozen.cur = s;
|
|
||||||
frozen.tokens = arr;
|
|
||||||
frozen.max_tokens = arr_len;
|
|
||||||
|
|
||||||
TRY(doit(&frozen));
|
|
||||||
|
|
||||||
return frozen.cur - s;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct json_token *parse_json2(const char *s, int s_len) {
|
|
||||||
struct frozen frozen;
|
|
||||||
|
|
||||||
memset(&frozen, 0, sizeof(frozen));
|
|
||||||
frozen.end = s + s_len;
|
|
||||||
frozen.cur = s;
|
|
||||||
frozen.do_realloc = 1;
|
|
||||||
|
|
||||||
if (doit(&frozen) < 0) {
|
|
||||||
FROZEN_FREE((void *) frozen.tokens);
|
|
||||||
frozen.tokens = NULL;
|
|
||||||
}
|
|
||||||
return frozen.tokens;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int path_part_len(const char *p) {
|
|
||||||
int i = 0;
|
|
||||||
while (p[i] != '\0' && p[i] != '[' && p[i] != '.') i++;
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct json_token *find_json_token(struct json_token *toks, const char *path) {
|
|
||||||
while (path != 0 && path[0] != '\0') {
|
|
||||||
int i, ind2 = 0, ind = -1, skip = 2, n = path_part_len(path);
|
|
||||||
if (path[0] == '[') {
|
|
||||||
if (toks->type != JSON_TYPE_ARRAY || !is_digit(path[1])) return 0;
|
|
||||||
for (ind = 0, n = 1; path[n] != ']' && path[n] != '\0'; n++) {
|
|
||||||
if (!is_digit(path[n])) return 0;
|
|
||||||
ind *= 10;
|
|
||||||
ind += path[n] - '0';
|
|
||||||
}
|
|
||||||
if (path[n++] != ']') return 0;
|
|
||||||
skip = 1; /* In objects, we skip 2 elems while iterating, in arrays 1. */
|
|
||||||
} else if (toks->type != JSON_TYPE_OBJECT)
|
|
||||||
return 0;
|
|
||||||
toks++;
|
|
||||||
for (i = 0; i < toks[-1].num_desc; i += skip, ind2++) {
|
|
||||||
/* ind == -1 indicated that we're iterating an array, not object */
|
|
||||||
if (ind == -1 && toks[i].type != JSON_TYPE_STRING) return 0;
|
|
||||||
if (ind2 == ind ||
|
|
||||||
(ind == -1 && toks[i].len == n && compare(path, toks[i].ptr, n))) {
|
|
||||||
i += skip - 1;
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
if (toks[i - 1 + skip].type == JSON_TYPE_ARRAY ||
|
|
||||||
toks[i - 1 + skip].type == JSON_TYPE_OBJECT) {
|
|
||||||
i += toks[i - 1 + skip].num_desc;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (i == toks[-1].num_desc) return 0;
|
|
||||||
path += n;
|
|
||||||
if (path[0] == '.') path++;
|
|
||||||
if (path[0] == '\0') return &toks[i];
|
|
||||||
toks += i;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int json_emit_long(char *buf, int buf_len, long int value) {
|
|
||||||
char tmp[20];
|
|
||||||
int n = snprintf(tmp, sizeof(tmp), "%ld", value);
|
|
||||||
strncpy(buf, tmp, buf_len > 0 ? buf_len : 0);
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
int json_emit_double(char *buf, int buf_len, double value) {
|
|
||||||
char tmp[20];
|
|
||||||
int n = snprintf(tmp, sizeof(tmp), "%g", value);
|
|
||||||
strncpy(buf, tmp, buf_len > 0 ? buf_len : 0);
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
int json_emit_quoted_str(char *s, int s_len, const char *str, int len) {
|
|
||||||
const char *begin = s, *end = s + s_len, *str_end = str + len;
|
|
||||||
char ch;
|
|
||||||
|
|
||||||
#define EMIT(x) \
|
|
||||||
do { \
|
|
||||||
if (s < end) *s = x; \
|
|
||||||
s++; \
|
|
||||||
} while (0)
|
|
||||||
|
|
||||||
EMIT('"');
|
|
||||||
while (str < str_end) {
|
|
||||||
ch = *str++;
|
|
||||||
switch (ch) {
|
|
||||||
case '"':
|
|
||||||
EMIT('\\');
|
|
||||||
EMIT('"');
|
|
||||||
break;
|
|
||||||
case '\\':
|
|
||||||
EMIT('\\');
|
|
||||||
EMIT('\\');
|
|
||||||
break;
|
|
||||||
case '\b':
|
|
||||||
EMIT('\\');
|
|
||||||
EMIT('b');
|
|
||||||
break;
|
|
||||||
case '\f':
|
|
||||||
EMIT('\\');
|
|
||||||
EMIT('f');
|
|
||||||
break;
|
|
||||||
case '\n':
|
|
||||||
EMIT('\\');
|
|
||||||
EMIT('n');
|
|
||||||
break;
|
|
||||||
case '\r':
|
|
||||||
EMIT('\\');
|
|
||||||
EMIT('r');
|
|
||||||
break;
|
|
||||||
case '\t':
|
|
||||||
EMIT('\\');
|
|
||||||
EMIT('t');
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
EMIT(ch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EMIT('"');
|
|
||||||
if (s < end) {
|
|
||||||
*s = '\0';
|
|
||||||
}
|
|
||||||
|
|
||||||
return s - begin;
|
|
||||||
}
|
|
||||||
|
|
||||||
int json_emit_unquoted_str(char *buf, int buf_len, const char *str, int len) {
|
|
||||||
if (buf_len > 0 && len > 0) {
|
|
||||||
int n = len < buf_len ? len : buf_len;
|
|
||||||
memcpy(buf, str, n);
|
|
||||||
if (n < buf_len) {
|
|
||||||
buf[n] = '\0';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return len;
|
|
||||||
}
|
|
||||||
|
|
||||||
int json_emit_va(char *s, int s_len, const char *fmt, va_list ap) {
|
|
||||||
const char *end = s + s_len, *str, *orig = s;
|
|
||||||
size_t len;
|
|
||||||
|
|
||||||
while (*fmt != '\0') {
|
|
||||||
switch (*fmt) {
|
|
||||||
case '[':
|
|
||||||
case ']':
|
|
||||||
case '{':
|
|
||||||
case '}':
|
|
||||||
case ',':
|
|
||||||
case ':':
|
|
||||||
case ' ':
|
|
||||||
case '\r':
|
|
||||||
case '\n':
|
|
||||||
case '\t':
|
|
||||||
if (s < end) {
|
|
||||||
*s = *fmt;
|
|
||||||
}
|
|
||||||
s++;
|
|
||||||
break;
|
|
||||||
case 'i':
|
|
||||||
s += json_emit_long(s, end - s, va_arg(ap, long) );
|
|
||||||
break;
|
|
||||||
case 'f':
|
|
||||||
s += json_emit_double(s, end - s, va_arg(ap, double) );
|
|
||||||
break;
|
|
||||||
case 'v':
|
|
||||||
str = va_arg(ap, char *);
|
|
||||||
len = va_arg(ap, size_t);
|
|
||||||
s += json_emit_quoted_str(s, end - s, str, len);
|
|
||||||
break;
|
|
||||||
case 'V':
|
|
||||||
str = va_arg(ap, char *);
|
|
||||||
len = va_arg(ap, size_t);
|
|
||||||
s += json_emit_unquoted_str(s, end - s, str, len);
|
|
||||||
break;
|
|
||||||
case 's':
|
|
||||||
str = va_arg(ap, char *);
|
|
||||||
s += json_emit_quoted_str(s, end - s, str, strlen(str));
|
|
||||||
break;
|
|
||||||
case 'S':
|
|
||||||
str = va_arg(ap, char *);
|
|
||||||
s += json_emit_unquoted_str(s, end - s, str, strlen(str));
|
|
||||||
break;
|
|
||||||
case 'T':
|
|
||||||
s += json_emit_unquoted_str(s, end - s, "true", 4);
|
|
||||||
break;
|
|
||||||
case 'F':
|
|
||||||
s += json_emit_unquoted_str(s, end - s, "false", 5);
|
|
||||||
break;
|
|
||||||
case 'N':
|
|
||||||
s += json_emit_unquoted_str(s, end - s, "null", 4);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
fmt++;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Best-effort to 0-terminate generated string */
|
|
||||||
if (s < end) {
|
|
||||||
*s = '\0';
|
|
||||||
}
|
|
||||||
|
|
||||||
return s - orig;
|
|
||||||
}
|
|
||||||
|
|
||||||
int json_emit(char *buf, int buf_len, const char *fmt, ...) {
|
|
||||||
int len;
|
|
||||||
va_list ap;
|
|
||||||
|
|
||||||
va_start(ap, fmt);
|
|
||||||
len = json_emit_va(buf, buf_len, fmt, ap);
|
|
||||||
va_end(ap);
|
|
||||||
|
|
||||||
return len;
|
|
||||||
}
|
|
||||||
#ifdef MG_MODULE_LINES
|
|
||||||
#line 1 "./src/../../common/md5.c"
|
#line 1 "./src/../../common/md5.c"
|
||||||
#endif
|
#endif
|
||||||
/*
|
/*
|
||||||
@ -8245,167 +7648,6 @@ struct mg_str mg_mk_str(const char *s) {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
#ifdef MG_MODULE_LINES
|
#ifdef MG_MODULE_LINES
|
||||||
#line 1 "./src/json-rpc.c"
|
|
||||||
#endif
|
|
||||||
/* Copyright (c) 2014 Cesanta Software Limited */
|
|
||||||
/* All rights reserved */
|
|
||||||
|
|
||||||
#ifndef MG_DISABLE_JSON_RPC
|
|
||||||
|
|
||||||
/* Amalgamated: #include "mongoose/src/internal.h" */
|
|
||||||
/* Amalgamated: #include "mongoose/src/json-rpc.h" */
|
|
||||||
/* Amalgamated: #include "mongoose/deps/frozen/frozen.h" */
|
|
||||||
|
|
||||||
int mg_rpc_create_reply(char *buf, int len, const struct mg_rpc_request *req,
|
|
||||||
const char *result_fmt, ...) {
|
|
||||||
static const struct json_token null_tok = {"null", 4, 0, JSON_TYPE_NULL};
|
|
||||||
const struct json_token *id = req->id == NULL ? &null_tok : req->id;
|
|
||||||
va_list ap;
|
|
||||||
int n = 0;
|
|
||||||
|
|
||||||
n += json_emit(buf + n, len - n, "{s:s,s:", "jsonrpc", "2.0", "id");
|
|
||||||
if (id->type == JSON_TYPE_STRING) {
|
|
||||||
n += json_emit_quoted_str(buf + n, len - n, id->ptr, id->len);
|
|
||||||
} else {
|
|
||||||
n += json_emit_unquoted_str(buf + n, len - n, id->ptr, id->len);
|
|
||||||
}
|
|
||||||
n += json_emit(buf + n, len - n, ",s:", "result");
|
|
||||||
|
|
||||||
va_start(ap, result_fmt);
|
|
||||||
n += json_emit_va(buf + n, len - n, result_fmt, ap);
|
|
||||||
va_end(ap);
|
|
||||||
|
|
||||||
n += json_emit(buf + n, len - n, "}");
|
|
||||||
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
int mg_rpc_create_request(char *buf, int len, const char *method,
|
|
||||||
const char *id, const char *params_fmt, ...) {
|
|
||||||
va_list ap;
|
|
||||||
int n = 0;
|
|
||||||
|
|
||||||
n += json_emit(buf + n, len - n, "{s:s,s:s,s:s,s:", "jsonrpc", "2.0", "id",
|
|
||||||
id, "method", method, "params");
|
|
||||||
va_start(ap, params_fmt);
|
|
||||||
n += json_emit_va(buf + n, len - n, params_fmt, ap);
|
|
||||||
va_end(ap);
|
|
||||||
|
|
||||||
n += json_emit(buf + n, len - n, "}");
|
|
||||||
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
int mg_rpc_create_error(char *buf, int len, struct mg_rpc_request *req,
|
|
||||||
int code, const char *message, const char *fmt, ...) {
|
|
||||||
va_list ap;
|
|
||||||
int n = 0;
|
|
||||||
|
|
||||||
n += json_emit(buf + n, len - n, "{s:s,s:V,s:{s:i,s:s,s:", "jsonrpc", "2.0",
|
|
||||||
"id", req->id == NULL ? "null" : req->id->ptr,
|
|
||||||
req->id == NULL ? 4 : req->id->len, "error", "code",
|
|
||||||
(long) code, "message", message, "data");
|
|
||||||
va_start(ap, fmt);
|
|
||||||
n += json_emit_va(buf + n, len - n, fmt, ap);
|
|
||||||
va_end(ap);
|
|
||||||
|
|
||||||
n += json_emit(buf + n, len - n, "}}");
|
|
||||||
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
int mg_rpc_create_std_error(char *buf, int len, struct mg_rpc_request *req,
|
|
||||||
int code) {
|
|
||||||
const char *message = NULL;
|
|
||||||
|
|
||||||
switch (code) {
|
|
||||||
case JSON_RPC_PARSE_ERROR:
|
|
||||||
message = "parse error";
|
|
||||||
break;
|
|
||||||
case JSON_RPC_INVALID_REQUEST_ERROR:
|
|
||||||
message = "invalid request";
|
|
||||||
break;
|
|
||||||
case JSON_RPC_METHOD_NOT_FOUND_ERROR:
|
|
||||||
message = "method not found";
|
|
||||||
break;
|
|
||||||
case JSON_RPC_INVALID_PARAMS_ERROR:
|
|
||||||
message = "invalid parameters";
|
|
||||||
break;
|
|
||||||
case JSON_RPC_SERVER_ERROR:
|
|
||||||
message = "server error";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
message = "unspecified error";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return mg_rpc_create_error(buf, len, req, code, message, "N");
|
|
||||||
}
|
|
||||||
|
|
||||||
int mg_rpc_dispatch(const char *buf, int len, char *dst, int dst_len,
|
|
||||||
const char **methods, mg_rpc_handler_t *handlers) {
|
|
||||||
struct json_token tokens[200];
|
|
||||||
struct mg_rpc_request req;
|
|
||||||
int i, n;
|
|
||||||
|
|
||||||
memset(&req, 0, sizeof(req));
|
|
||||||
n = parse_json(buf, len, tokens, sizeof(tokens) / sizeof(tokens[0]));
|
|
||||||
if (n <= 0) {
|
|
||||||
int err_code = (n == JSON_STRING_INVALID) ? JSON_RPC_PARSE_ERROR
|
|
||||||
: JSON_RPC_SERVER_ERROR;
|
|
||||||
return mg_rpc_create_std_error(dst, dst_len, &req, err_code);
|
|
||||||
}
|
|
||||||
|
|
||||||
req.message = tokens;
|
|
||||||
req.id = find_json_token(tokens, "id");
|
|
||||||
req.method = find_json_token(tokens, "method");
|
|
||||||
req.params = find_json_token(tokens, "params");
|
|
||||||
|
|
||||||
if (req.id == NULL || req.method == NULL) {
|
|
||||||
return mg_rpc_create_std_error(dst, dst_len, &req,
|
|
||||||
JSON_RPC_INVALID_REQUEST_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (i = 0; methods[i] != NULL; i++) {
|
|
||||||
int mlen = strlen(methods[i]);
|
|
||||||
if (mlen == req.method->len &&
|
|
||||||
memcmp(methods[i], req.method->ptr, mlen) == 0)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (methods[i] == NULL) {
|
|
||||||
return mg_rpc_create_std_error(dst, dst_len, &req,
|
|
||||||
JSON_RPC_METHOD_NOT_FOUND_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
return handlers[i](dst, dst_len, &req);
|
|
||||||
}
|
|
||||||
|
|
||||||
int mg_rpc_parse_reply(const char *buf, int len, struct json_token *toks,
|
|
||||||
int max_toks, struct mg_rpc_reply *rep,
|
|
||||||
struct mg_rpc_error *er) {
|
|
||||||
int n = parse_json(buf, len, toks, max_toks);
|
|
||||||
|
|
||||||
memset(rep, 0, sizeof(*rep));
|
|
||||||
memset(er, 0, sizeof(*er));
|
|
||||||
|
|
||||||
if (n > 0) {
|
|
||||||
if ((rep->result = find_json_token(toks, "result")) != NULL) {
|
|
||||||
rep->message = toks;
|
|
||||||
rep->id = find_json_token(toks, "id");
|
|
||||||
} else {
|
|
||||||
er->message = toks;
|
|
||||||
er->id = find_json_token(toks, "id");
|
|
||||||
er->error_code = find_json_token(toks, "error.code");
|
|
||||||
er->error_message = find_json_token(toks, "error.message");
|
|
||||||
er->error_data = find_json_token(toks, "error.data");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif /* MG_DISABLE_JSON_RPC */
|
|
||||||
#ifdef MG_MODULE_LINES
|
|
||||||
#line 1 "./src/mqtt.c"
|
#line 1 "./src/mqtt.c"
|
||||||
#endif
|
#endif
|
||||||
/*
|
/*
|
||||||
|
206
mongoose.h
206
mongoose.h
@ -1067,74 +1067,6 @@ const char *c_strnstr(const char *s, const char *find, size_t slen);
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#endif /* CS_COMMON_STR_UTIL_H_ */
|
#endif /* CS_COMMON_STR_UTIL_H_ */
|
||||||
/*
|
|
||||||
* Copyright (c) 2004-2013 Sergey Lyubka <valenok@gmail.com>
|
|
||||||
* Copyright (c) 2013 Cesanta Software Limited
|
|
||||||
* All rights reserved
|
|
||||||
*
|
|
||||||
* This library is dual-licensed: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License version 2 as
|
|
||||||
* published by the Free Software Foundation. For the terms of this
|
|
||||||
* license, see <http: *www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
* You are free to use this library under the terms of the GNU General
|
|
||||||
* Public License, but WITHOUT ANY WARRANTY; without even the implied
|
|
||||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* Alternatively, you can license this library under a commercial
|
|
||||||
* license, as set out in <http://cesanta.com/products.html>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef CS_MONGOOSE_DEPS_FROZEN_FROZEN_H_
|
|
||||||
#define CS_MONGOOSE_DEPS_FROZEN_FROZEN_H_
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif /* __cplusplus */
|
|
||||||
|
|
||||||
#include <stdarg.h>
|
|
||||||
|
|
||||||
enum json_type {
|
|
||||||
JSON_TYPE_EOF = 0, /* End of parsed tokens marker */
|
|
||||||
JSON_TYPE_STRING = 1,
|
|
||||||
JSON_TYPE_NUMBER = 2,
|
|
||||||
JSON_TYPE_OBJECT = 3,
|
|
||||||
JSON_TYPE_TRUE = 4,
|
|
||||||
JSON_TYPE_FALSE = 5,
|
|
||||||
JSON_TYPE_NULL = 6,
|
|
||||||
JSON_TYPE_ARRAY = 7
|
|
||||||
};
|
|
||||||
|
|
||||||
struct json_token {
|
|
||||||
const char *ptr; /* Points to the beginning of the token */
|
|
||||||
int len; /* Token length */
|
|
||||||
int num_desc; /* For arrays and object, total number of descendants */
|
|
||||||
enum json_type type; /* Type of the token, possible values above */
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Error codes */
|
|
||||||
#define JSON_STRING_INVALID -1
|
|
||||||
#define JSON_STRING_INCOMPLETE -2
|
|
||||||
#define JSON_TOKEN_ARRAY_TOO_SMALL -3
|
|
||||||
|
|
||||||
int parse_json(const char *json_string, int json_string_length,
|
|
||||||
struct json_token *tokens_array, int size_of_tokens_array);
|
|
||||||
struct json_token *parse_json2(const char *json_string, int string_length);
|
|
||||||
struct json_token *find_json_token(struct json_token *toks, const char *path);
|
|
||||||
|
|
||||||
int json_emit_long(char *buf, int buf_len, long value);
|
|
||||||
int json_emit_double(char *buf, int buf_len, double value);
|
|
||||||
int json_emit_quoted_str(char *buf, int buf_len, const char *str, int len);
|
|
||||||
int json_emit_unquoted_str(char *buf, int buf_len, const char *str, int len);
|
|
||||||
int json_emit(char *buf, int buf_len, const char *fmt, ...);
|
|
||||||
int json_emit_va(char *buf, int buf_len, const char *fmt, va_list);
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
}
|
|
||||||
#endif /* __cplusplus */
|
|
||||||
|
|
||||||
#endif /* CS_MONGOOSE_DEPS_FROZEN_FROZEN_H_ */
|
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2014 Cesanta Software Limited
|
* Copyright (c) 2014 Cesanta Software Limited
|
||||||
* All rights reserved
|
* All rights reserved
|
||||||
@ -2754,144 +2686,6 @@ int mg_http_check_digest_auth(struct http_message *hm, const char *auth_domain,
|
|||||||
}
|
}
|
||||||
#endif /* __cplusplus */
|
#endif /* __cplusplus */
|
||||||
#endif /* CS_MONGOOSE_SRC_HTTP_H_ */
|
#endif /* CS_MONGOOSE_SRC_HTTP_H_ */
|
||||||
/*
|
|
||||||
* Copyright (c) 2014 Cesanta Software Limited
|
|
||||||
* All rights reserved
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* === JSON-RPC
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef CS_MONGOOSE_SRC_JSON_RPC_H_
|
|
||||||
#define CS_MONGOOSE_SRC_JSON_RPC_H_
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif /* __cplusplus */
|
|
||||||
|
|
||||||
/* JSON-RPC request */
|
|
||||||
struct mg_rpc_request {
|
|
||||||
struct json_token *message; /* Whole RPC message */
|
|
||||||
struct json_token *id; /* Message ID */
|
|
||||||
struct json_token *method; /* Method name */
|
|
||||||
struct json_token *params; /* Method params */
|
|
||||||
};
|
|
||||||
|
|
||||||
/* JSON-RPC response */
|
|
||||||
struct mg_rpc_reply {
|
|
||||||
struct json_token *message; /* Whole RPC message */
|
|
||||||
struct json_token *id; /* Message ID */
|
|
||||||
struct json_token *result; /* Remote call result */
|
|
||||||
};
|
|
||||||
|
|
||||||
/* JSON-RPC error */
|
|
||||||
struct mg_rpc_error {
|
|
||||||
struct json_token *message; /* Whole RPC message */
|
|
||||||
struct json_token *id; /* Message ID */
|
|
||||||
struct json_token *error_code; /* error.code */
|
|
||||||
struct json_token *error_message; /* error.message */
|
|
||||||
struct json_token *error_data; /* error.data, can be NULL */
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Parse JSON-RPC reply contained in `buf`, `len` into JSON tokens array
|
|
||||||
* `toks`, `max_toks`. If buffer contains valid reply, `reply` structure is
|
|
||||||
* populated. The result of RPC call is located in `reply.result`. On error,
|
|
||||||
* `error` structure is populated. Returns: the result of calling
|
|
||||||
* `parse_json(buf, len, toks, max_toks)`:
|
|
||||||
*
|
|
||||||
* On success, an offset inside `json_string` is returned
|
|
||||||
* where parsing has finished. On failure, a negative number is
|
|
||||||
* returned, one of:
|
|
||||||
*
|
|
||||||
* - `#define JSON_STRING_INVALID -1`
|
|
||||||
* - `#define JSON_STRING_INCOMPLETE -2`
|
|
||||||
* - `#define JSON_TOKEN_ARRAY_TOO_SMALL -3`
|
|
||||||
*/
|
|
||||||
int mg_rpc_parse_reply(const char *buf, int len, struct json_token *toks,
|
|
||||||
int max_toks, struct mg_rpc_reply *,
|
|
||||||
struct mg_rpc_error *);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Create JSON-RPC request in a given buffer.
|
|
||||||
*
|
|
||||||
* Return length of the request, which
|
|
||||||
* can be larger then `len` that indicates an overflow.
|
|
||||||
* `params_fmt` format string should conform to `json_emit()` API,
|
|
||||||
* see https://github.com/cesanta/frozen
|
|
||||||
*/
|
|
||||||
int mg_rpc_create_request(char *buf, int len, const char *method,
|
|
||||||
const char *id, const char *params_fmt, ...);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Create JSON-RPC reply in a given buffer.
|
|
||||||
*
|
|
||||||
* Return length of the reply, which
|
|
||||||
* can be larger then `len` that indicates an overflow.
|
|
||||||
* `result_fmt` format string should conform to `json_emit()` API,
|
|
||||||
* see https://github.com/cesanta/frozen
|
|
||||||
*/
|
|
||||||
int mg_rpc_create_reply(char *buf, int len, const struct mg_rpc_request *req,
|
|
||||||
const char *result_fmt, ...);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Create JSON-RPC error reply in a given buffer.
|
|
||||||
*
|
|
||||||
* Return length of the error, which
|
|
||||||
* can be larger then `len` that indicates an overflow.
|
|
||||||
* `fmt` format string should conform to `json_emit()` API,
|
|
||||||
* see https://github.com/cesanta/frozen
|
|
||||||
*/
|
|
||||||
int mg_rpc_create_error(char *buf, int len, struct mg_rpc_request *req,
|
|
||||||
int code, const char *message, const char *fmt, ...);
|
|
||||||
|
|
||||||
/* JSON-RPC standard error codes */
|
|
||||||
#define JSON_RPC_PARSE_ERROR (-32700)
|
|
||||||
#define JSON_RPC_INVALID_REQUEST_ERROR (-32600)
|
|
||||||
#define JSON_RPC_METHOD_NOT_FOUND_ERROR (-32601)
|
|
||||||
#define JSON_RPC_INVALID_PARAMS_ERROR (-32602)
|
|
||||||
#define JSON_RPC_INTERNAL_ERROR (-32603)
|
|
||||||
#define JSON_RPC_SERVER_ERROR (-32000)
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Create JSON-RPC error in a given buffer.
|
|
||||||
*
|
|
||||||
* Return length of the error, which
|
|
||||||
* can be larger then `len` that indicates an overflow. See
|
|
||||||
* JSON_RPC_*_ERROR definitions for standard error values:
|
|
||||||
*
|
|
||||||
* - `#define JSON_RPC_PARSE_ERROR (-32700)`
|
|
||||||
* - `#define JSON_RPC_INVALID_REQUEST_ERROR (-32600)`
|
|
||||||
* - `#define JSON_RPC_METHOD_NOT_FOUND_ERROR (-32601)`
|
|
||||||
* - `#define JSON_RPC_INVALID_PARAMS_ERROR (-32602)`
|
|
||||||
* - `#define JSON_RPC_INTERNAL_ERROR (-32603)`
|
|
||||||
* - `#define JSON_RPC_SERVER_ERROR (-32000)`
|
|
||||||
*/
|
|
||||||
int mg_rpc_create_std_error(char *buf, int len, struct mg_rpc_request *req,
|
|
||||||
int code);
|
|
||||||
|
|
||||||
typedef int (*mg_rpc_handler_t)(char *buf, int len, struct mg_rpc_request *req);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Dispatches a JSON-RPC request.
|
|
||||||
*
|
|
||||||
* Parses JSON-RPC request contained in `buf`, `len`.
|
|
||||||
* Then, dispatches the request to the correct handler method.
|
|
||||||
* Valid method names should be specified in NULL
|
|
||||||
* terminated array `methods`, and corresponding handlers in `handlers`.
|
|
||||||
* Result is put in `dst`, `dst_len`. Return: length of the result, which
|
|
||||||
* can be larger then `dst_len` that indicates an overflow.
|
|
||||||
* Overflown bytes are not written to the buffer.
|
|
||||||
* If method is not found, an error is automatically generated.
|
|
||||||
*/
|
|
||||||
int mg_rpc_dispatch(const char *buf, int, char *dst, int dst_len,
|
|
||||||
const char **methods, mg_rpc_handler_t *handlers);
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
}
|
|
||||||
#endif /* __cplusplus */
|
|
||||||
#endif /* CS_MONGOOSE_SRC_JSON_RPC_H_ */
|
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2014 Cesanta Software Limited
|
* Copyright (c) 2014 Cesanta Software Limited
|
||||||
* All rights reserved
|
* All rights reserved
|
||||||
|
Loading…
x
Reference in New Issue
Block a user