522 lines
16 KiB
C
522 lines
16 KiB
C
|
/*
|
||
|
* Copyright (c) 2011-2012, Swedish Institute of Computer Science.
|
||
|
* All rights reserved.
|
||
|
*
|
||
|
* Redistribution and use in source and binary forms, with or without
|
||
|
* modification, are permitted provided that the following conditions
|
||
|
* are met:
|
||
|
* 1. Redistributions of source code must retain the above copyright
|
||
|
* notice, this list of conditions and the following disclaimer.
|
||
|
* 2. Redistributions in binary form must reproduce the above copyright
|
||
|
* notice, this list of conditions and the following disclaimer in the
|
||
|
* documentation and/or other materials provided with the distribution.
|
||
|
* 3. Neither the name of the Institute nor the names of its contributors
|
||
|
* may be used to endorse or promote products derived from this software
|
||
|
* without specific prior written permission.
|
||
|
*
|
||
|
* THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND
|
||
|
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
|
||
|
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||
|
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||
|
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||
|
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||
|
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||
|
* SUCH DAMAGE.
|
||
|
*
|
||
|
* This file is part of the Contiki operating system.
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* \file
|
||
|
* JSON webservice util
|
||
|
* \author
|
||
|
* Niclas Finne <nfi@sics.se>
|
||
|
* Joakim Eriksson <joakime@sics.se>
|
||
|
* Joel Hoglund <joel@sics.se>
|
||
|
*/
|
||
|
|
||
|
#include "contiki.h"
|
||
|
#if PLATFORM_HAS_LEDS
|
||
|
#include "dev/leds.h"
|
||
|
#endif
|
||
|
#include "httpd-ws.h"
|
||
|
#include "jsontree.h"
|
||
|
#include "jsonparse.h"
|
||
|
#include "json-ws.h"
|
||
|
#include <stdio.h>
|
||
|
#include <string.h>
|
||
|
|
||
|
#define DEBUG 0
|
||
|
#if DEBUG
|
||
|
#define PRINTF(...) printf(__VA_ARGS__)
|
||
|
#else
|
||
|
#define PRINTF(...)
|
||
|
#endif
|
||
|
|
||
|
#ifdef JSON_WS_CONF_CALLBACK_PROTO
|
||
|
#define CALLBACK_PROTO JSON_WS_CONF_CALLBACK_PROTO
|
||
|
#else
|
||
|
#define CALLBACK_PROTO "http"
|
||
|
#endif /* JSON_WS_CONF_CALLBACK_PROTO */
|
||
|
|
||
|
#ifdef JSON_WS_CONF_CALLBACK_PORT
|
||
|
#define CALLBACK_PORT JSON_WS_CONF_CALLBACK_PORT
|
||
|
#else
|
||
|
#define CALLBACK_PORT 8080;
|
||
|
#endif /* JSON_WS_CONF_CALLBACK_PORT */
|
||
|
|
||
|
/* Predefined startup-send interval */
|
||
|
#ifdef JSON_WS_CONF_CALLBACK_INTERVAL
|
||
|
#define SEND_INTERVAL JSON_WS_CONF_CALLBACK_INTERVAL
|
||
|
#else
|
||
|
#define SEND_INTERVAL 120
|
||
|
#endif
|
||
|
|
||
|
static const char http_content_type_json[] = "application/json";
|
||
|
|
||
|
/* Maximum 40 chars in host name?: 5 x 8 */
|
||
|
static char callback_host[40] = "[aaaa::1]";
|
||
|
static uint16_t callback_port = CALLBACK_PORT;
|
||
|
static uint16_t callback_interval = SEND_INTERVAL;
|
||
|
static char callback_path[80] = "/debug/";
|
||
|
static char callback_appdata[80] = "";
|
||
|
static char callback_proto[8] = CALLBACK_PROTO;
|
||
|
static const char *callback_json_path = NULL;
|
||
|
static struct jsontree_object *tree;
|
||
|
static struct ctimer periodic_timer;
|
||
|
long json_time_offset = 0;
|
||
|
|
||
|
/* support for submitting to cosm */
|
||
|
#if WITH_COSM
|
||
|
extern struct jsontree_callback cosm_value_callback;
|
||
|
|
||
|
JSONTREE_OBJECT_EXT(cosm_tree,
|
||
|
JSONTREE_PAIR("current_value", &cosm_value_callback));
|
||
|
#endif /* WITH_COSM */
|
||
|
|
||
|
static void periodic(void *ptr);
|
||
|
|
||
|
/*---------------------------------------------------------------------------*/
|
||
|
static void
|
||
|
json_copy_string(struct jsonparse_state *parser, char *string, int len)
|
||
|
{
|
||
|
jsonparse_next(parser);
|
||
|
jsonparse_next(parser);
|
||
|
jsonparse_copy_value(parser, string, len);
|
||
|
}
|
||
|
/*---------------------------------------------------------------------------*/
|
||
|
static int
|
||
|
cfg_get(struct jsontree_context *js_ctx)
|
||
|
{
|
||
|
const char *path = jsontree_path_name(js_ctx, js_ctx->depth - 1);
|
||
|
|
||
|
if(strncmp(path, "host", 4) == 0) {
|
||
|
jsontree_write_string(js_ctx, callback_host);
|
||
|
} else if(strncmp(path, "port", 4) == 0) {
|
||
|
jsontree_write_int(js_ctx, callback_port);
|
||
|
} else if(strncmp(path, "interval", 8) == 0) {
|
||
|
jsontree_write_int(js_ctx, callback_interval);
|
||
|
} else if(strncmp(path, "path", 4) == 0) {
|
||
|
jsontree_write_string(js_ctx, callback_path);
|
||
|
} else if(strncmp(path, "appdata", 7) == 0) {
|
||
|
jsontree_write_string(js_ctx, callback_appdata[0] == '\0' ? "" : "***");
|
||
|
} else if(strncmp(path, "proto", 5) == 0) {
|
||
|
jsontree_write_string(js_ctx, callback_proto);
|
||
|
}
|
||
|
return 0;
|
||
|
}
|
||
|
static int
|
||
|
cfg_set(struct jsontree_context *js_ctx, struct jsonparse_state *parser)
|
||
|
{
|
||
|
int type;
|
||
|
int update = 0;
|
||
|
|
||
|
while((type = jsonparse_next(parser)) != 0) {
|
||
|
if(type == JSON_TYPE_PAIR_NAME) {
|
||
|
if(jsonparse_strcmp_value(parser, "host") == 0) {
|
||
|
json_copy_string(parser, callback_host, sizeof(callback_host));
|
||
|
update++;
|
||
|
} else if(jsonparse_strcmp_value(parser, "path") == 0) {
|
||
|
json_copy_string(parser, callback_path, sizeof(callback_path));
|
||
|
update++;
|
||
|
} else if(jsonparse_strcmp_value(parser, "appdata") == 0) {
|
||
|
json_copy_string(parser, callback_appdata, sizeof(callback_appdata));
|
||
|
update++;
|
||
|
} else if(jsonparse_strcmp_value(parser, "proto") == 0) {
|
||
|
json_copy_string(parser, callback_proto, sizeof(callback_proto));
|
||
|
update++;
|
||
|
} else if(jsonparse_strcmp_value(parser, "port") == 0) {
|
||
|
jsonparse_next(parser);
|
||
|
jsonparse_next(parser);
|
||
|
callback_port = jsonparse_get_value_as_int(parser);
|
||
|
if(callback_port == 0) {
|
||
|
callback_port = CALLBACK_PORT;
|
||
|
}
|
||
|
update++;
|
||
|
} else if(jsonparse_strcmp_value(parser, "interval") == 0) {
|
||
|
jsonparse_next(parser);
|
||
|
jsonparse_next(parser);
|
||
|
callback_interval = jsonparse_get_value_as_int(parser);
|
||
|
if(callback_interval == 0) {
|
||
|
callback_interval = SEND_INTERVAL;
|
||
|
}
|
||
|
update++;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if(update && callback_json_path != NULL) {
|
||
|
#if WITH_UDP
|
||
|
if(strncmp(callback_proto, "udp", 3) == 0) {
|
||
|
json_ws_udp_setup(callback_host, callback_port);
|
||
|
}
|
||
|
#endif
|
||
|
ctimer_set(&periodic_timer, CLOCK_SECOND * callback_interval,
|
||
|
periodic, NULL);
|
||
|
}
|
||
|
return 0;
|
||
|
}
|
||
|
static struct jsontree_callback cfg_callback =
|
||
|
JSONTREE_CALLBACK(cfg_get, cfg_set);
|
||
|
|
||
|
JSONTREE_OBJECT_EXT(json_subscribe_callback,
|
||
|
JSONTREE_PAIR("host", &cfg_callback),
|
||
|
JSONTREE_PAIR("port", &cfg_callback),
|
||
|
JSONTREE_PAIR("path", &cfg_callback),
|
||
|
JSONTREE_PAIR("appdata", &cfg_callback),
|
||
|
JSONTREE_PAIR("proto", &cfg_callback),
|
||
|
JSONTREE_PAIR("interval", &cfg_callback));
|
||
|
/*---------------------------------------------------------------------------*/
|
||
|
static int
|
||
|
time_get(struct jsontree_context *js_ctx)
|
||
|
{
|
||
|
/* unix time */
|
||
|
char buf[20];
|
||
|
unsigned long time = json_time_offset + clock_seconds();
|
||
|
|
||
|
snprintf(buf, 20, "%lu", time);
|
||
|
jsontree_write_atom(js_ctx, buf);
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
static int
|
||
|
time_set(struct jsontree_context *js_ctx, struct jsonparse_state *parser)
|
||
|
{
|
||
|
int type;
|
||
|
unsigned long time;
|
||
|
|
||
|
while((type = jsonparse_next(parser)) != 0) {
|
||
|
if(type == JSON_TYPE_PAIR_NAME) {
|
||
|
if(jsonparse_strcmp_value(parser, "time") == 0) {
|
||
|
jsonparse_next(parser);
|
||
|
jsonparse_next(parser);
|
||
|
time = jsonparse_get_value_as_long(parser);
|
||
|
json_time_offset = time - clock_seconds();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
struct jsontree_callback json_time_callback =
|
||
|
JSONTREE_CALLBACK(time_get, time_set);
|
||
|
/*---------------------------------------------------------------------------*/
|
||
|
#if PLATFORM_HAS_LEDS
|
||
|
#include "dev/leds.h"
|
||
|
|
||
|
static int
|
||
|
ws_leds_get(struct jsontree_context *js_ctx)
|
||
|
{
|
||
|
char buf[4];
|
||
|
unsigned char leds = leds_get();
|
||
|
|
||
|
snprintf(buf, 4, "%u", leds);
|
||
|
jsontree_write_atom(js_ctx, buf);
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
static int
|
||
|
ws_leds_set(struct jsontree_context *js_ctx, struct jsonparse_state *parser)
|
||
|
{
|
||
|
int type, old_leds, new_leds;
|
||
|
|
||
|
while((type = jsonparse_next(parser)) != 0) {
|
||
|
if(type == JSON_TYPE_PAIR_NAME) {
|
||
|
if(jsonparse_strcmp_value(parser, "leds") == 0) {
|
||
|
jsonparse_next(parser);
|
||
|
jsonparse_next(parser);
|
||
|
new_leds = jsonparse_get_value_as_int(parser);
|
||
|
old_leds = leds_get();
|
||
|
leds_on(~old_leds & new_leds);
|
||
|
leds_off(old_leds & ~new_leds);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
struct jsontree_callback json_leds_callback =
|
||
|
JSONTREE_CALLBACK(ws_leds_get, ws_leds_set);
|
||
|
|
||
|
#endif /* PLATFORM_HAS_LEDS */
|
||
|
/*---------------------------------------------------------------------------*/
|
||
|
static struct httpd_ws_state *json_putchar_context;
|
||
|
static int
|
||
|
json_putchar(int c)
|
||
|
{
|
||
|
if(json_putchar_context != NULL &&
|
||
|
json_putchar_context->outbuf_pos < HTTPD_OUTBUF_SIZE) {
|
||
|
json_putchar_context->outbuf[json_putchar_context->outbuf_pos++] = c;
|
||
|
return c;
|
||
|
}
|
||
|
return 0;
|
||
|
}
|
||
|
static int putchar_size = 0;
|
||
|
static int
|
||
|
json_putchar_count(int c)
|
||
|
{
|
||
|
putchar_size++;
|
||
|
return c;
|
||
|
}
|
||
|
/*---------------------------------------------------------------------------*/
|
||
|
static
|
||
|
PT_THREAD(send_values(struct httpd_ws_state *s))
|
||
|
{
|
||
|
json_putchar_context = s;
|
||
|
|
||
|
PSOCK_BEGIN(&s->sout);
|
||
|
|
||
|
s->json.putchar = json_putchar;
|
||
|
s->outbuf_pos = 0;
|
||
|
|
||
|
if(s->json.values[0] == NULL) {
|
||
|
/* Nothing to do */
|
||
|
|
||
|
} else if(s->request_type == HTTPD_WS_POST &&
|
||
|
s->state == HTTPD_WS_STATE_OUTPUT) {
|
||
|
/* Set value */
|
||
|
struct jsontree_value *v;
|
||
|
struct jsontree_callback *c;
|
||
|
|
||
|
while((v = jsontree_find_next(&s->json, JSON_TYPE_CALLBACK)) != NULL) {
|
||
|
c = (struct jsontree_callback *)v;
|
||
|
if(c->set != NULL) {
|
||
|
struct jsonparse_state js;
|
||
|
|
||
|
jsonparse_setup(&js, s->inputbuf, s->content_len);
|
||
|
c->set(&s->json, &js);
|
||
|
}
|
||
|
}
|
||
|
memcpy(s->outbuf, "{\"Status\":\"OK\"}", 15);
|
||
|
s->outbuf_pos = 15;
|
||
|
|
||
|
} else {
|
||
|
/* Get value */
|
||
|
while(jsontree_print_next(&s->json) && s->json.path <= s->json.depth) {
|
||
|
if(s->outbuf_pos >= UIP_TCP_MSS) {
|
||
|
SEND_STRING(&s->sout, s->outbuf, UIP_TCP_MSS);
|
||
|
s->outbuf_pos -= UIP_TCP_MSS;
|
||
|
if(s->outbuf_pos > 0) {
|
||
|
memcpy(s->outbuf, &s->outbuf[UIP_TCP_MSS], s->outbuf_pos);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if(s->outbuf_pos > 0) {
|
||
|
SEND_STRING(&s->sout, s->outbuf, s->outbuf_pos);
|
||
|
s->outbuf_pos = 0;
|
||
|
}
|
||
|
PSOCK_END(&s->sout);
|
||
|
}
|
||
|
/*---------------------------------------------------------------------------*/
|
||
|
struct jsontree_value *
|
||
|
find_json_path(struct jsontree_context *json, const char *path)
|
||
|
{
|
||
|
struct jsontree_value *v;
|
||
|
const char *start;
|
||
|
const char *end;
|
||
|
int len;
|
||
|
|
||
|
v = json->values[0];
|
||
|
start = path;
|
||
|
do {
|
||
|
end = strchr(start, '/');
|
||
|
if(end == start) {
|
||
|
break;
|
||
|
}
|
||
|
if(end != NULL) {
|
||
|
len = end - start;
|
||
|
end++;
|
||
|
} else {
|
||
|
len = strlen(start);
|
||
|
}
|
||
|
if(v->type != JSON_TYPE_OBJECT) {
|
||
|
v = NULL;
|
||
|
} else {
|
||
|
struct jsontree_object *o;
|
||
|
int i;
|
||
|
|
||
|
o = (struct jsontree_object *)v;
|
||
|
v = NULL;
|
||
|
for(i = 0; i < o->count; i++) {
|
||
|
if(strncmp(start, o->pairs[i].name, len) == 0) {
|
||
|
v = o->pairs[i].value;
|
||
|
json->index[json->depth] = i;
|
||
|
json->depth++;
|
||
|
json->values[json->depth] = v;
|
||
|
json->index[json->depth] = 0;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
start = end;
|
||
|
} while(end != NULL && *end != '\0' && v != NULL);
|
||
|
json->callback_state = 0;
|
||
|
return v;
|
||
|
}
|
||
|
/*---------------------------------------------------------------------------*/
|
||
|
static int
|
||
|
calculate_json_size(const char *path, struct jsontree_value *v)
|
||
|
{
|
||
|
/* check size of JSON expression */
|
||
|
struct jsontree_context json;
|
||
|
|
||
|
json.values[0] = (v == NULL) ? (struct jsontree_value *)tree : v;
|
||
|
jsontree_reset(&json);
|
||
|
|
||
|
if(path != NULL) {
|
||
|
find_json_path(&json, path);
|
||
|
}
|
||
|
|
||
|
json.path = json.depth;
|
||
|
json.putchar = json_putchar_count;
|
||
|
putchar_size = 0;
|
||
|
while(jsontree_print_next(&json) && json.path <= json.depth);
|
||
|
|
||
|
return putchar_size;
|
||
|
}
|
||
|
/*---------------------------------------------------------------------------*/
|
||
|
httpd_ws_script_t
|
||
|
httpd_ws_get_script(struct httpd_ws_state *s)
|
||
|
{
|
||
|
struct jsontree_value *v;
|
||
|
|
||
|
s->json.values[0] = v = (struct jsontree_value *)tree;
|
||
|
jsontree_reset(&s->json);
|
||
|
|
||
|
if(s->filename[1] == '\0') {
|
||
|
/* Default page: show full JSON tree. */
|
||
|
} else {
|
||
|
v = find_json_path(&s->json, &s->filename[1]);
|
||
|
}
|
||
|
if(v != NULL) {
|
||
|
s->json.path = s->json.depth;
|
||
|
s->content_type = http_content_type_json;
|
||
|
return send_values;
|
||
|
}
|
||
|
return NULL;
|
||
|
}
|
||
|
/*---------------------------------------------------------------------------*/
|
||
|
#if JSON_POST_EXTRA_HEADER || WITH_COSM
|
||
|
static int
|
||
|
output_headers(struct httpd_ws_state *s, char *buffer, int buffer_size,
|
||
|
int index)
|
||
|
{
|
||
|
if(index == 0) {
|
||
|
#ifdef JSON_POST_EXTRA_HEADER
|
||
|
return snprintf(buffer, buffer_size, "%s\r\n", JSON_POST_EXTRA_HEADER);
|
||
|
} else if(index == 1) {
|
||
|
#endif
|
||
|
#if WITH_COSM
|
||
|
if(strncmp(callback_proto, "cosm", 4) == 0 && callback_appdata[0] != '\0') {
|
||
|
return snprintf(buffer, buffer_size, "X-PachubeApiKey:%s\r\n",
|
||
|
callback_appdata);
|
||
|
}
|
||
|
#endif
|
||
|
}
|
||
|
return 0;
|
||
|
}
|
||
|
#endif /* JSON_POST_EXTRA_HEADER || WITH_COSM */
|
||
|
/*---------------------------------------------------------------------------*/
|
||
|
static void
|
||
|
periodic(void *ptr)
|
||
|
{
|
||
|
struct httpd_ws_state *s;
|
||
|
int callback_size;
|
||
|
|
||
|
if(callback_json_path != NULL && strlen(callback_host) > 2) {
|
||
|
ctimer_restart(&periodic_timer);
|
||
|
|
||
|
if(strncmp(callback_proto, "http", 4) == 0) {
|
||
|
callback_size = calculate_json_size(callback_json_path, NULL);
|
||
|
s = httpd_ws_request(HTTPD_WS_POST, callback_host, NULL, callback_port,
|
||
|
callback_path, http_content_type_json,
|
||
|
callback_size, send_values);
|
||
|
if(s != NULL) {
|
||
|
PRINTF("PERIODIC POST %s\n", callback_json_path);
|
||
|
#if JSON_POST_EXTRA_HEADER
|
||
|
s->output_extra_headers = output_headers;
|
||
|
#endif
|
||
|
s->json.values[0] = (struct jsontree_value *)tree;
|
||
|
jsontree_reset(&s->json);
|
||
|
find_json_path(&s->json, callback_json_path);
|
||
|
s->json.path = s->json.depth;
|
||
|
} else {
|
||
|
PRINTF("PERIODIC CALLBACK FAILED\n");
|
||
|
}
|
||
|
#if WITH_COSM
|
||
|
} else if(strncmp(callback_proto, "cosm", 4) == 0) {
|
||
|
callback_size = calculate_json_size(NULL, (struct jsontree_value *)
|
||
|
&cosm_tree);
|
||
|
/* printf("JSON Size:%d\n", callback_size); */
|
||
|
s = httpd_ws_request(HTTPD_WS_PUT, callback_host, "api.pachube.com",
|
||
|
callback_port, callback_path,
|
||
|
http_content_type_json, callback_size, send_values);
|
||
|
/* host = cosm host */
|
||
|
/* path => path to datastream / data point */
|
||
|
s->output_extra_headers = output_headers;
|
||
|
s->json.values[0] = (struct jsontree_value *)&cosm_tree;
|
||
|
jsontree_reset(&s->json);
|
||
|
s->json.path = 0;
|
||
|
|
||
|
PRINTF("PERIODIC cosm callback: %d\n", callback_size);
|
||
|
#endif /* WITH_COSM */
|
||
|
}
|
||
|
#if WITH_UDP
|
||
|
else {
|
||
|
callback_size = calculate_json_size(callback_json_path, NULL);
|
||
|
PRINTF("PERIODIC UDP size: %d\n", callback_size);
|
||
|
json_ws_udp_send(tree, callback_json_path);
|
||
|
}
|
||
|
#endif /* WITH_UDP */
|
||
|
} else {
|
||
|
printf("PERIODIC CALLBACK - nothing todo\n");
|
||
|
}
|
||
|
}
|
||
|
/*---------------------------------------------------------------------------*/
|
||
|
void
|
||
|
json_ws_init(struct jsontree_object *json)
|
||
|
{
|
||
|
PRINTF("JSON INIT (callback %s every %u seconds)\n",
|
||
|
CALLBACK_PROTO, SEND_INTERVAL);
|
||
|
tree = json;
|
||
|
ctimer_set(&periodic_timer, CLOCK_SECOND * SEND_INTERVAL, periodic, NULL);
|
||
|
process_start(&httpd_ws_process, NULL);
|
||
|
#if WITH_UDP
|
||
|
if(strncmp(callback_proto, "udp", 3) == 0) {
|
||
|
json_ws_udp_setup(callback_host, callback_port);
|
||
|
}
|
||
|
#endif /* WITH_UDP */
|
||
|
}
|
||
|
/*---------------------------------------------------------------------------*/
|
||
|
void
|
||
|
json_ws_set_callback(const char *path)
|
||
|
{
|
||
|
callback_json_path = path;
|
||
|
ctimer_restart(&periodic_timer);
|
||
|
}
|
||
|
/*---------------------------------------------------------------------------*/
|