Index: ChangeLog
===================================================================
--- ChangeLog	(revision 5895)
+++ ChangeLog	(revision 5902)
@@ -2,4 +2,6 @@
 	* Added build summery feature to RABS and configure script.
 	* Moved away from roar_libroar_get_path_static().
+	* Added GPIO service.
+	* Added GPIO driver: piface.
 
 v. 1.0beta9 - Mon Feb 18 2013 23:28 CET
Index: configure
===================================================================
--- configure	(revision 5893)
+++ configure	(revision 5902)
@@ -663,4 +663,5 @@
 test_lib_defmake ROAR_HAVE_H_NETINET_IN  %            'netinet/in.h' c         -- 'netinet/in.h'
 test_lib_defmake ROAR_HAVE_H_LINUX_REBOOT %            linux/reboot.h c        -- linux/reboot.h
+test_lib_defmake ROAR_HAVE_H_LINUX_SPI_SPIDEV %            linux/spi/spidev.h c        -- linux/spi/spidev.h
 test_lib_defmake ROAR_HAVE_H_WINDOWS     %            windows.h     c          -- windows.h
 test_lib_defmake ROAR_HAVE_H_WINSOCK     %            winsock.h     c          -- winsock.h
Index: include/libroar/services.h
===================================================================
--- include/libroar/services.h	(revision 5887)
+++ include/libroar/services.h	(revision 5902)
@@ -212,4 +212,80 @@
 };
 
+// GPIO/Sensors:
+#define ROAR_SERVICE_GPIO_NAME "gpio"
+#define ROAR_SERVICE_GPIO_ABI  "1.0beta10-pr0"
+
+#define ROAR_SERVICE_GPIO_FINPUT    ((uint_least32_t)0x00000001UL)
+#define ROAR_SERVICE_GPIO_FOUTPUT   ((uint_least32_t)0x00000002UL)
+#define ROAR_SERVICE_GPIO_FPULLDOWN ((uint_least32_t)0x00000004UL)
+#define ROAR_SERVICE_GPIO_FPULLUP   ((uint_least32_t)0x00000008UL)
+#define ROAR_SERVICE_GPIO_FCACHED   ((uint_least32_t)0x00000010UL)
+
+enum roar_service_gpio_type {
+ ROAR_SERVICE_GPIO_TERROR = -1,
+ ROAR_SERVICE_GPIO_TUNKNOWN = 0,
+ ROAR_SERVICE_GPIO_TINT,
+ ROAR_SERVICE_GPIO_TENUM,
+ ROAR_SERVICE_GPIO_TBOOL,
+ ROAR_SERVICE_GPIO_TTRISTATE,
+ ROAR_SERVICE_GPIO_TFLOAT
+};
+
+enum roar_service_gpio_state {
+ ROAR_SERVICE_GPIO_SERROR = -1,
+ ROAR_SERVICE_GPIO_SUNKNOWN = 0,
+ ROAR_SERVICE_GPIO_SFREE,
+ ROAR_SERVICE_GPIO_SUNINITED,
+ ROAR_SERVICE_GPIO_SREADY,
+ ROAR_SERVICE_GPIO_SBUSY,
+ ROAR_SERVICE_GPIO_SDISCONNECTED,
+ ROAR_SERVICE_GPIO_SDISABLED,
+ ROAR_SERVICE_GPIO_SINERROR
+};
+
+struct roar_service_gpio_port {
+ int id;
+ const char * name;
+ uint_least32_t mode;
+ const char * unit;
+ enum roar_service_gpio_type type;
+ enum roar_service_gpio_state state;
+ int irange_min;
+ int irange_max;
+ double frange_min;
+ double frange_max;
+};
+
+struct roar_service_gpio {
+ // get list of gpio IDs.
+ // buffer is passed as ids, buffer size (in elements) is passed as len.
+ // returns the number of elements stored in ids or -1 on error.
+ ssize_t (*list)(int * ids, size_t len);
+ // get the number of gpios. See also comments above on what.
+ ssize_t (*num)(enum roar_service_num what);
+ // get a gpio by ID. The object returned is a copy and must not be motified or freed.
+ int (*get)(int id, struct roar_service_gpio_port * port);
+
+ // Sets up ports.
+ // the controler should set mode and state of port to given target mode and state.
+ // If id is given as -1 this is about the controler. mode MUST NOT contain any
+ // flags beside ROAR_SERVICE_GPIO_FCACHED. If ROAR_SERVICE_GPIO_FCACHED
+ // is set it is applied all ports.
+ int (*setup)(int id, uint_least32_t mode, enum roar_service_gpio_state state);
+
+ // get value of port (input or output) as int or float.
+ int (*get_int)(int id);
+ double (*get_float)(int id);
+
+ // set value of output (as int or float).
+ int (*set_int)(int id, int val);
+ int (*set_float)(int id, double val);
+
+ // convert the value val as returned by the port to the base unit of the port.
+ // if rev is set converst from the base unit to the value as used by the port.
+ int (*convert_int)(int id, int val, int rev);
+ double (*convert_float)(int id, double val, int rev);
+};
+
 #endif
 
Index: plugins/universal/Makefile
===================================================================
--- plugins/universal/Makefile	(revision 5851)
+++ plugins/universal/Makefile	(revision 5902)
@@ -4,6 +4,7 @@
 TARGETS_PROTO=protocol-echo$(SHARED_SUFFIX) protocol-daytime$(SHARED_SUFFIX) protocol-discard$(SHARED_SUFFIX) tic-tac-toe$(SHARED_SUFFIX) protocol-http$(SHARED_SUFFIX)
 TARGETS_MISC=plugin-info$(SHARED_SUFFIX) helloworld$(SHARED_SUFFIX) debug-notify$(SHARED_SUFFIX)
+TARGETS_IO=piface$(SHARED_SUFFIX)
 TARGETS_SERVICE=service-about$(SHARED_SUFFIX)
-TARGETS=$(TARGETS_MISC) $(TARGETS_PROTO) $(TARGETS_SERVICE)
+TARGETS=$(TARGETS_MISC) $(TARGETS_PROTO) $(TARGETS_SERVICE) $(TARGETS_IO)
 
 #DEFINES        = -DDEBUG
Index: plugins/universal/piface.c
===================================================================
--- plugins/universal/piface.c	(revision 5902)
+++ plugins/universal/piface.c	(revision 5902)
@@ -0,0 +1,374 @@
+//piface.c:
+
+/*
+ *      Copyright (C) Philipp 'ph3-der-loewe' Schafft - 2013
+ *
+ *  This file is part of roard a part of RoarAudio,
+ *  a cross-platform sound system for both, home and professional use.
+ *  See README for details.
+ *
+ *  This file is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License version 3
+ *  as published by the Free Software Foundation.
+ *
+ *  RoarAudio is distributed in the hope that it will be useful,
+ *  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.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this software; see the file COPYING.  If not, write to
+ *  the Free Software Foundation, 51 Franklin Street, Fifth Floor,
+ *  Boston, MA 02110-1301, USA.
+ *
+ */
+
+#include <roaraudio.h>
+
+#ifdef ROAR_HAVE_H_LINUX_SPI_SPIDEV
+#include <linux/spi/spidev.h>
+#include <sys/ioctl.h>
+
+#define NUM_PORTS (8+8+1)
+
+#define TRANSFER_LEN   3
+#define TRANSFER_DELAY 5
+#define TRANSFER_SPEED 1000000
+#define TRANSFER_BPW   8
+
+#define SPI_WRITE_CMD 0x40
+#define SPI_READ_CMD 0x41
+
+#define IODIRA 0x00 // I/O direction A
+#define IODIRB 0x01 // I/O direction B
+#define IOCON  0x0A // I/O config
+#define GPIOA  0x12 // port A
+#define GPIOB  0x13 // port B
+#define GPPUA  0x0C // port A pullups
+#define GPPUB  0x0D // port B pullups
+#define OUTPUT_PORT GPIOA
+#define INPUT_PORT  GPIOB
+
+struct state {
+ int inited;
+ int bus;
+ int device;
+ struct roar_vio_calls vio_store;
+ struct roar_vio_calls * vio;
+ unsigned char buffer_output;
+ unsigned char buffer_input;
+ struct roar_service_gpio_port ports[NUM_PORTS];
+};
+
+static struct state * state = NULL;
+static struct state   state_init = {
+ .inited = 0,
+ .bus    = 0,
+ .device = 0,
+ .vio    = NULL,
+ .buffer_output = 0x00,
+ .buffer_input  = 0x00,
+ .ports  = {
+// inputs:
+  {.id = 0, .name = "gpin0", .mode = ROAR_SERVICE_GPIO_FINPUT|ROAR_SERVICE_GPIO_FPULLUP, .unit = NULL, .type = ROAR_SERVICE_GPIO_TBOOL, .state = ROAR_SERVICE_GPIO_SUNINITED, .irange_min = 0, .irange_max = 1, .frange_min = 0, .frange_max = 1},
+  {.id = 1, .name = "gpin1", .mode = ROAR_SERVICE_GPIO_FINPUT|ROAR_SERVICE_GPIO_FPULLUP, .unit = NULL, .type = ROAR_SERVICE_GPIO_TBOOL, .state = ROAR_SERVICE_GPIO_SUNINITED, .irange_min = 0, .irange_max = 1, .frange_min = 0, .frange_max = 1},
+  {.id = 2, .name = "gpin2", .mode = ROAR_SERVICE_GPIO_FINPUT|ROAR_SERVICE_GPIO_FPULLUP, .unit = NULL, .type = ROAR_SERVICE_GPIO_TBOOL, .state = ROAR_SERVICE_GPIO_SUNINITED, .irange_min = 0, .irange_max = 1, .frange_min = 0, .frange_max = 1},
+  {.id = 3, .name = "gpin3", .mode = ROAR_SERVICE_GPIO_FINPUT|ROAR_SERVICE_GPIO_FPULLUP, .unit = NULL, .type = ROAR_SERVICE_GPIO_TBOOL, .state = ROAR_SERVICE_GPIO_SUNINITED, .irange_min = 0, .irange_max = 1, .frange_min = 0, .frange_max = 1},
+  {.id = 4, .name = "gpin4", .mode = ROAR_SERVICE_GPIO_FINPUT|ROAR_SERVICE_GPIO_FPULLUP, .unit = NULL, .type = ROAR_SERVICE_GPIO_TBOOL, .state = ROAR_SERVICE_GPIO_SUNINITED, .irange_min = 0, .irange_max = 1, .frange_min = 0, .frange_max = 1},
+  {.id = 5, .name = "gpin5", .mode = ROAR_SERVICE_GPIO_FINPUT|ROAR_SERVICE_GPIO_FPULLUP, .unit = NULL, .type = ROAR_SERVICE_GPIO_TBOOL, .state = ROAR_SERVICE_GPIO_SUNINITED, .irange_min = 0, .irange_max = 1, .frange_min = 0, .frange_max = 1},
+  {.id = 6, .name = "gpin6", .mode = ROAR_SERVICE_GPIO_FINPUT|ROAR_SERVICE_GPIO_FPULLUP, .unit = NULL, .type = ROAR_SERVICE_GPIO_TBOOL, .state = ROAR_SERVICE_GPIO_SUNINITED, .irange_min = 0, .irange_max = 1, .frange_min = 0, .frange_max = 1},
+  {.id = 7, .name = "gpin7", .mode = ROAR_SERVICE_GPIO_FINPUT|ROAR_SERVICE_GPIO_FPULLUP, .unit = NULL, .type = ROAR_SERVICE_GPIO_TBOOL, .state = ROAR_SERVICE_GPIO_SUNINITED, .irange_min = 0, .irange_max = 1, .frange_min = 0, .frange_max = 1},
+
+// outputs:
+  {.id = 8, .name = "gpout0", .mode = ROAR_SERVICE_GPIO_FOUTPUT, .unit = NULL, .type = ROAR_SERVICE_GPIO_TBOOL, .state = ROAR_SERVICE_GPIO_SUNINITED, .irange_min = 0, .irange_max = 1, .frange_min = 0, .frange_max = 1},
+  {.id = 9, .name = "gpout1", .mode = ROAR_SERVICE_GPIO_FOUTPUT, .unit = NULL, .type = ROAR_SERVICE_GPIO_TBOOL, .state = ROAR_SERVICE_GPIO_SUNINITED, .irange_min = 0, .irange_max = 1, .frange_min = 0, .frange_max = 1},
+  {.id = 10, .name = "gpout2", .mode = ROAR_SERVICE_GPIO_FOUTPUT, .unit = NULL, .type = ROAR_SERVICE_GPIO_TBOOL, .state = ROAR_SERVICE_GPIO_SUNINITED, .irange_min = 0, .irange_max = 1, .frange_min = 0, .frange_max = 1},
+  {.id = 11, .name = "gpout3", .mode = ROAR_SERVICE_GPIO_FOUTPUT, .unit = NULL, .type = ROAR_SERVICE_GPIO_TBOOL, .state = ROAR_SERVICE_GPIO_SUNINITED, .irange_min = 0, .irange_max = 1, .frange_min = 0, .frange_max = 1},
+  {.id = 12, .name = "gpout4", .mode = ROAR_SERVICE_GPIO_FOUTPUT, .unit = NULL, .type = ROAR_SERVICE_GPIO_TBOOL, .state = ROAR_SERVICE_GPIO_SUNINITED, .irange_min = 0, .irange_max = 1, .frange_min = 0, .frange_max = 1},
+  {.id = 13, .name = "gpout5", .mode = ROAR_SERVICE_GPIO_FOUTPUT, .unit = NULL, .type = ROAR_SERVICE_GPIO_TBOOL, .state = ROAR_SERVICE_GPIO_SUNINITED, .irange_min = 0, .irange_max = 1, .frange_min = 0, .frange_max = 1},
+  {.id = 14, .name = "gpout6", .mode = ROAR_SERVICE_GPIO_FOUTPUT, .unit = NULL, .type = ROAR_SERVICE_GPIO_TBOOL, .state = ROAR_SERVICE_GPIO_SUNINITED, .irange_min = 0, .irange_max = 1, .frange_min = 0, .frange_max = 1},
+  {.id = 15, .name = "gpout7", .mode = ROAR_SERVICE_GPIO_FOUTPUT, .unit = NULL, .type = ROAR_SERVICE_GPIO_TBOOL, .state = ROAR_SERVICE_GPIO_SUNINITED, .irange_min = 0, .irange_max = 1, .frange_min = 0, .frange_max = 1},
+
+// special:
+  {.id = 16, .name = "bar0", .mode = ROAR_SERVICE_GPIO_FOUTPUT, .unit = NULL, .type = ROAR_SERVICE_GPIO_TINT, .state = ROAR_SERVICE_GPIO_SUNINITED, .irange_min = 0, .irange_max = 8, .frange_min = 0, .frange_max = 8}
+ }
+};
+
+static int piface__transfer(char cmd, char port, char value) {
+ unsigned char txbuffer[3] = {cmd, port, value};
+ unsigned char rxbuffer[3];
+ struct spi_ioc_transfer transfer_buffer = {
+  .tx_buf = (unsigned long) txbuffer,
+  .rx_buf = (unsigned long) rxbuffer,
+  .len = TRANSFER_LEN,
+  .delay_usecs = TRANSFER_DELAY,
+  .speed_hz = TRANSFER_SPEED,
+  .bits_per_word = TRANSFER_BPW,
+ };
+ struct roar_vio_sysio_ioctl ctl = {.cmd = SPI_IOC_MESSAGE(1), .argp = &transfer_buffer};
+
+ if ( roar_vio_ctl(state->vio, ROAR_VIO_CTL_SYSIO_IOCTL, &ctl) == -1 )
+  return -1; 
+
+ return (unsigned int)rxbuffer[2];
+}
+
+static int piface__init(void) {
+ char path[80];
+
+ if ( state->inited ) {
+  roar_err_set(ROAR_ERROR_ALREADY);
+  return -1;
+ }
+
+ snprintf(path, sizeof(path), "file://dev/spidev%i.%i", state->bus, state->device);
+
+ state->vio = &(state->vio_store);
+ if ( roar_vio_open_dstr_simple(state->vio, path, ROAR_VIOF_READWRITE) == -1 )
+  return -1;
+
+ piface__transfer(SPI_WRITE_CMD, IOCON,  8);    // enable hardware addressing
+ piface__transfer(SPI_WRITE_CMD, GPIOA,  0x00); // turn on port A
+ piface__transfer(SPI_WRITE_CMD, IODIRA, 0);    // set port A as an output
+ piface__transfer(SPI_WRITE_CMD, IODIRB, 0xFF); // set port B as an input
+ piface__transfer(SPI_WRITE_CMD, GPPUB,  0xFF); // turn on port B pullups
+ piface__transfer(SPI_WRITE_CMD, OUTPUT_PORT, 0x00); // turn of all outputs.
+
+ state->inited = 1;
+
+ return 0;
+}
+
+static int piface__free(void) {
+ if ( !state->inited ) {
+  roar_err_set(ROAR_ERROR_BADSTATE);
+  return -1;
+ }
+
+ roar_vio_close(state->vio);
+
+ state->inited = 0;
+
+ return 0;
+}
+
+static void piface__write_output(unsigned char val) {
+ piface__transfer(SPI_WRITE_CMD, OUTPUT_PORT, state->buffer_output = val);
+}
+
+static unsigned char piface__read_output(void) {
+ return state->buffer_output = piface__transfer(SPI_READ_CMD, OUTPUT_PORT, 0xFF);
+}
+
+static unsigned char piface__read_input(void) {
+ return state->buffer_input = piface__transfer(SPI_READ_CMD, INPUT_PORT, 0xFF);
+}
+
+static inline int __portval_to_bool(const struct roar_service_gpio_port * port, unsigned int val) {
+ int bit = 1 << (port->id & 0x07);
+ return val & bit ? 1 : 0;
+}
+
+ // get list of gpio IDs.
+ // buffer is passed as ids, buffer size (in elements) is passed as len.
+ // returns the number of elements stored in ids or -1 on error.
+static ssize_t __list(int * ids, size_t len) {
+ size_t i;
+
+ if ( ids == NULL ) {
+  roar_err_set(ROAR_ERROR_FAULT);
+  return -1;
+ }
+
+ for (i = 0; i < len && i < NUM_PORTS; i++)
+  ids[i] = i;
+
+ return i;
+}
+
+ // get the number of gpios. See also comments above on what.
+static ssize_t __num(enum roar_service_num what) {
+ (void)what;
+
+ return NUM_PORTS; // 8 inputs, 8 outputs and a a virtual bar
+}
+ // get a gpio by ID. The object returned is a copy and must not be motified or freed.
+static int __get(int id, struct roar_service_gpio_port * port) {
+ if ( id < 0 || id >= NUM_PORTS ) {
+  roar_err_set(ROAR_ERROR_RANGE);
+  return -1;
+ }
+
+ if ( port == NULL ) {
+  roar_err_set(ROAR_ERROR_FAULT);
+  return -1;
+ }
+
+ *port = state->ports[id];
+ return 0;
+}
+
+ // Sets up ports.
+ // the controler should set mode and state of port to given target mode and state.
+ // If id is given as -1 this is about the controler. mode MUST NOT contain any
+ // flags beside ROAR_SERVICE_GPIO_FCACHED. If ROAR_SERVICE_GPIO_FCACHED
+ // is set it is applied all ports.
+static int __setup(int id, uint_least32_t mode, enum roar_service_gpio_state tstate) {
+ size_t i;
+
+ if ( id < -1 || id >= NUM_PORTS ) {
+  roar_err_set(ROAR_ERROR_RANGE);
+  return -1;
+ }
+
+ if ( id == -1 ) {
+  if ( mode != 0 && mode != ROAR_SERVICE_GPIO_FCACHED ) {
+   roar_err_set(ROAR_ERROR_INVAL);
+   return -1;
+  }
+
+  switch (tstate) {
+   case ROAR_SERVICE_GPIO_SREADY:
+    if ( state->inited )
+     return 0;
+    if ( piface__init() == -1 )
+     return -1;
+    for (i = 0; i < NUM_PORTS; i++) {
+     state->ports[i].mode |= ROAR_SERVICE_GPIO_FCACHED;
+     state->ports[i].mode -= ROAR_SERVICE_GPIO_FCACHED;
+     state->ports[i].mode |= mode;
+    }
+    return 0;
+   case ROAR_SERVICE_GPIO_SUNINITED:
+    if ( !state->inited )
+     return 0;
+    return piface__free();
+   default:
+     roar_err_set(ROAR_ERROR_INVAL);
+     return -1;
+    break;
+  }
+ }
+
+ roar_err_set(ROAR_ERROR_NOSYS);
+ return -1;
+}
+
+ // get value of port (input or output) as int or float.
+static int __get_int(int id) {
+ const struct roar_service_gpio_port * port;
+ unsigned char val;
+
+ if ( !state->inited ) {
+  roar_err_set(ROAR_ERROR_BADSTATE);
+  return -1;
+ }
+
+ if ( id < 0 || id >= NUM_PORTS ) {
+  roar_err_set(ROAR_ERROR_RANGE);
+  return -1;
+ }
+
+ port = &(state->ports[id]);
+
+ if ( port->type != ROAR_SERVICE_GPIO_TBOOL ) {
+  roar_err_set(ROAR_ERROR_NOTSUP);
+  return -1;
+ }
+
+ if ( port->mode & ROAR_SERVICE_GPIO_FINPUT ) {
+  val = piface__read_input();
+ } else {
+  val = piface__read_output();
+ }
+
+ return __portval_to_bool(port, val);
+}
+
+static double __get_float(int id) {
+ return __get_int(id);
+}
+
+ // set value of output (as int or float).
+static int __set_int(int id, int val) {
+ const struct roar_service_gpio_port * port;
+ unsigned char buffer;
+ int bit;
+
+ if ( !state->inited ) {
+  roar_err_set(ROAR_ERROR_BADSTATE);
+  return -1;
+ }
+
+ if ( id < 0 || id >= NUM_PORTS ) {
+  roar_err_set(ROAR_ERROR_RANGE);
+  return -1;
+ }
+
+ port = &(state->ports[id]);
+
+ if ( !(port->mode & ROAR_SERVICE_GPIO_FOUTPUT) || port->type != ROAR_SERVICE_GPIO_TBOOL ) {
+  roar_err_set(ROAR_ERROR_NOTSUP);
+  return -1;
+ }
+
+ bit = 1 << (id & 0x07);
+
+ buffer = state->buffer_output;
+ buffer |= bit;
+
+ if ( !val )
+  buffer -= bit;
+
+ piface__write_output(buffer);
+
+ return 0;
+}
+
+static int __set_float(int id, double val) {
+ return __set_int(id, val);
+}
+
+static struct roar_service_gpio api = {
+ .list = __list,
+ .num  = __num,
+ .get  = __get,
+ .setup = __setup,
+ .get_int = __get_int,
+ .get_float = __get_float,
+ .set_int = __set_int,
+ .set_float = __set_float,
+ .convert_int = NULL,
+ .convert_float = NULL
+};
+
+ROAR_DL_PLUGIN_REG_SERVICES_GET_API(get_api, api)
+
+static const struct roar_dl_service service[1] = {
+ {
+  .appname = NULL,
+  .appabi = NULL,
+  .servicename = ROAR_SERVICE_GPIO_NAME,
+  .serviceabi = ROAR_SERVICE_GPIO_ABI,
+  .description = "Interface to PiFace",
+  .flags = ROAR_DL_SERVICE_FLAGS_NONE,
+  .userdata = NULL,
+  .get_api = get_api
+ }
+};
+
+ROAR_DL_PLUGIN_REG_SERVICES(service);
+
+ROAR_DL_PLUGIN_START(piface) {
+ ROAR_DL_PLUGIN_META_PRODUCT_NIV("piface", ROAR_VID_ROARAUDIO, ROAR_VNAME_ROARAUDIO);
+ ROAR_DL_PLUGIN_META_VERSION(ROAR_VERSION_STRING);
+ ROAR_DL_PLUGIN_META_LICENSE_TAG(GPLv3_0);
+ ROAR_DL_PLUGIN_META_CONTACT_FLNE("Philipp", "Schafft", "ph3-der-loewe", "lion@lion.leolix.org");
+ ROAR_DL_PLUGIN_META_DESC("This plugin provides a simple interface to the PiFace board.");
+ ROAR_DL_PLUGIN_REG_FNFUNC(ROAR_DL_FN_SERVICE);
+ ROAR_DL_PLUGIN_REG_GLOBAL_DATA(state, state_init);
+} ROAR_DL_PLUGIN_END
+
+#endif
+
+//ll
