EPICS Controls Argonne National Laboratory

Experimental Physics and
Industrial Control System

2002  2003  2004  2005  2006  2007  2008  2009  2010  2011  2012  2013  2014  2015  2016  2017  2018  2019  2020  <20212022  2023  2024  Index 2002  2003  2004  2005  2006  2007  2008  2009  2010  2011  2012  2013  2014  2015  2016  2017  2018  2019  2020  <20212022  2023  2024 
<== Date ==> <== Thread ==>

Subject: [Merge] ~bfrk/epics-base:address-modifiers into epics-base:7.0
From: Ben Franksen via Core-talk <core-talk at aps.anl.gov>
To: mp+400517 at code.launchpad.net
Date: Thu, 01 Apr 2021 09:32:22 -0000
Ben Franksen has proposed merging ~bfrk/epics-base:address-modifiers into epics-base:7.0.

Requested reviews:
  EPICS Core Developers (epics-core)

For more details, see:
https://code.launchpad.net/~bfrk/epics-base/+git/epics-base/+merge/400517

As discussed on core-talk, this enables interpretation of "array address modifiers" a la "PV[s:i:e]" when using dbChannelPut or dbChannelPutField. This means the feature works for all PV links (I changed DB links to call dbChannelPut) as well as for caput.

The work is based on Dirk's dbChannelForDBLink branch (merged into 7.0) and some of my previous work (see branch address-modifiers-prerequisites).

The current implementation is /very/ liberal in that it never fails if the address modifier is out-of-bounds wrt the (target) record field. In this case the put operation is truncated to the overlap between the modifier and the target array, which may be empty (i.e. no change in the target). Similarly, if the source array has too few elements or excess elements, relative to the modifier, the request gets truncated to the smaller of these numbers.

Another design decision was that a put operation with a modifier never changes the current number of elements of the target. So you cannot currently "extend" an array using a modifier by writing beyond its current size.

All of these particular behaviors are, of course, open for debate. I can easily change any aspect as the implementation is quite simple.
-- 
Your team EPICS Core Developers is requested to review the proposed merge of ~bfrk/epics-base:address-modifiers into epics-base:7.0.
diff --git a/modules/database/src/ioc/db/Makefile b/modules/database/src/ioc/db/Makefile
index ebb78e2..e590cc6 100644
--- a/modules/database/src/ioc/db/Makefile
+++ b/modules/database/src/ioc/db/Makefile
@@ -11,10 +11,12 @@
 
 SRC_DIRS += $(IOCDIR)/db
 
+INC += arrayRangeModifier.h
 INC += callback.h
 INC += dbAccess.h
 INC += dbAccessDefs.h
 INC += dbAddr.h
+INC += dbAddrModifier.h
 INC += dbBkpt.h
 INC += dbCa.h
 INC += dbChannel.h
@@ -66,6 +68,7 @@ HTMLS += dbCommonRecord.html
 HTMLS += dbCommonInput.html
 HTMLS += dbCommonOutput.html
 
+dbCore_SRCS += arrayRangeModifier.c
 dbCore_SRCS += dbLock.c
 dbCore_SRCS += dbAccess.c
 dbCore_SRCS += dbBkpt.c
diff --git a/modules/database/src/ioc/db/arrayRangeModifier.c b/modules/database/src/ioc/db/arrayRangeModifier.c
new file mode 100644
index 0000000..1feb7bc
--- /dev/null
+++ b/modules/database/src/ioc/db/arrayRangeModifier.c
@@ -0,0 +1,167 @@
+/*************************************************************************\
+* EPICS BASE is distributed subject to a Software License Agreement found
+* in file LICENSE that is included with this distribution.
+\*************************************************************************/
+
+#include <assert.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+#include "arrayRangeModifier.h"
+#include "dbAccessDefs.h"
+#include "dbConvertFast.h"
+#include "dbConvert.h"
+
+typedef struct {
+    long start;
+    long incr;
+    long end;
+} arrayRangeModifier;
+
+long wrapArrayIndices(long *start, const long increment,
+    long *end, const long no_elements)
+{
+    if (*start < 0) *start = no_elements + *start;
+    if (*start < 0) *start = 0;
+    if (*start > no_elements) *start = no_elements;
+
+    if (*end < 0) *end = no_elements + *end;
+    if (*end < 0) *end = 0;
+    if (*end >= no_elements) *end = no_elements - 1;
+
+    if (*end - *start >= 0)
+        return 1 + (*end - *start) / increment;
+    else
+        return 0;
+}
+
+static long handleArrayRangeModifier(DBADDR *paddr, short dbrType, const void
+    *pbuffer, long nRequest, void *pvt, long offset, long available)
+{
+    arrayRangeModifier *pmod = (arrayRangeModifier *)pvt;
+    long status = 0;
+    long start = pmod->start;
+    long end = pmod->end;
+    /* Note that this limits the return value to be <= available */
+    long n = wrapArrayIndices(&start, pmod->incr, &end, available);
+
+    assert(pmod->incr > 0);
+    if (pmod->incr > 1) {
+        long i, j;
+        long (*putCvt) (const void *from, void *to, const dbAddr * paddr) =
+            dbFastPutConvertRoutine[dbrType][paddr->field_type];
+        short dbr_size = dbValueSize(dbrType);
+        if (nRequest > n)
+            nRequest = n;
+        for (i = 0, j = (offset + start) % paddr->no_elements; i < nRequest;
+            i++, j = (j + pmod->incr) % paddr->no_elements) {
+            status = putCvt(pbuffer + (i * dbr_size),
+                paddr->pfield + (j * paddr->field_size), paddr);
+        }
+    } else {
+        offset = (offset + start) % paddr->no_elements;
+        if (nRequest > n)
+            nRequest = n;
+        if (paddr->no_elements <= 1) {
+            status = dbFastPutConvertRoutine[dbrType][paddr->field_type] (pbuffer,
+                paddr->pfield, paddr);
+        } else {
+            if (paddr->no_elements < nRequest)
+                nRequest = paddr->no_elements;
+            status = dbPutConvertRoutine[dbrType][paddr->field_type] (paddr,
+                pbuffer, nRequest, paddr->no_elements, offset);
+        }
+    }
+    return status;
+}
+
+long createArrayRangeModifier(dbAddrModifier *pmod, long start, long incr, long end)
+{
+    arrayRangeModifier *pvt =
+        (arrayRangeModifier *) malloc(sizeof(arrayRangeModifier));
+    if (incr <= 0) {
+        return S_db_errArg;
+    }
+    if (!pvt) {
+        return S_db_noMemory;
+    }
+    pvt->start = start;
+    pvt->incr = incr;
+    pvt->end = end;
+    pmod->pvt = pvt;
+    pmod->handle = handleArrayRangeModifier;
+    return 0;
+}
+
+void deleteArrayRangeModifier(dbAddrModifier *pmod)
+{
+    if (pmod->pvt)
+        free(pmod->pvt);
+}
+
+long parseArrayRange(dbAddrModifier *pmod, const char **pstring)
+{
+    long start = 0;
+    long end = -1;
+    long incr = 1;
+    long tmp;
+    const char *pcurrent = *pstring;
+    char *pnext;
+    ptrdiff_t advance;
+    long status = 0;
+
+    if (*pcurrent != '[') {
+        return -1;
+    }
+    pcurrent++;
+    /* If no number is present, strtol() returns 0 and sets pnext=pcurrent,
+       else pnext points to the first char after the number */
+    tmp = strtol(pcurrent, &pnext, 0);
+    advance = pnext - pcurrent;
+    if (advance) start = tmp;
+    pcurrent = pnext;
+    if (*pcurrent == ']' && advance) {
+        end = start;
+        goto done;
+    }
+    if (*pcurrent != ':') {
+        return -1;
+    }
+    pcurrent++;
+    tmp = strtol(pcurrent, &pnext, 0);
+    advance = pnext - pcurrent;
+    pcurrent = pnext;
+    if (*pcurrent == ']') {
+        if (advance) end = tmp;
+        goto done;
+    }
+    if (advance) incr = tmp;
+    if (*pcurrent != ':') {
+        return -1;
+    }
+    pcurrent++;
+    tmp = strtol(pcurrent, &pnext, 0);
+    advance = pnext - pcurrent;
+    if (advance) end = tmp;
+    pcurrent = pnext;
+    if (*pcurrent != ']') {
+        return -1;
+    }
+
+done:
+    status = createArrayRangeModifier(pmod, start, incr, end);
+    if (status) {
+        return status;
+    }
+    pcurrent++;
+    *pstring = pcurrent;
+    return 0;
+}
+
+void getArrayRange(dbAddrModifier *pmod, long *pstart, long *pincr, long *pend)
+{
+    arrayRangeModifier *pvt = (arrayRangeModifier *)pmod->pvt;
+    *pstart = pvt->start;
+    *pincr = pvt->incr;
+    *pend = pvt->end;
+}
diff --git a/modules/database/src/ioc/db/arrayRangeModifier.h b/modules/database/src/ioc/db/arrayRangeModifier.h
new file mode 100644
index 0000000..8e0cd2f
--- /dev/null
+++ b/modules/database/src/ioc/db/arrayRangeModifier.h
@@ -0,0 +1,69 @@
+/*************************************************************************\
+* EPICS BASE is distributed subject to a Software License Agreement found
+* in file LICENSE that is included with this distribution.
+\*************************************************************************/
+#ifndef ARRAYRANGEMODIFIER_H
+#define ARRAYRANGEMODIFIER_H
+
+#include "dbAddrModifier.h"
+#include "shareLib.h"
+
+/** @brief The array range address modifier. */
+
+/** @brief Given a number of elements, convert negative start and end indices
+ *         to non-negative ones by counting from the end of the array.
+ *
+ * @param start         Pointer to possibly negative start index
+ * @param increment     Increment
+ * @param end           Pointer to possibly negative end index
+ * @param no_elements   Number of array elements
+ * @return              Final number of elements
+ */
+epicsShareFunc
+long wrapArrayIndices(long *start, long increment, long *end, long no_elements);
+
+/** @brief Create an array range modifier from start index, increment, and end index
+ *
+ * @param pmod          Pointer to address modifier (user allocated)
+ * @param start         Start index (possibly negative)
+ * @param incr          Increment (must be positive)
+ * @param end           End index (possibly negative)
+ * @return              Status
+ */
+epicsShareFunc
+long createArrayRangeModifier(dbAddrModifier *pmod, long start, long incr, long end);
+
+/** @brief Alternative creation function that parses the data from a string.
+ *
+ * If successful, advances 'pstring' to point after the recognized address modifier.
+ *
+ * @param pmod          Pointer to uninitialized address modifier (user allocated)
+ * @param pstring       Pointer to string to parse (in/out)
+ * @return              0 (success) or -1 (failure)
+ */
+epicsShareFunc
+long parseArrayRange(dbAddrModifier *pmod, const char **pstring);
+
+/** @brief Extract the private data
+ *
+ * The 'pmod' argument must point to a valid array range address modifier.
+ * The other aruments must point to appropriately sized storage.
+ *
+ * @param pmod          Pointer to uninitialized address modifier (user allocated)
+ * @param pstart        Start index (out)
+ * @param pincr         Increment (out)
+ * @param pend          End index (out)
+ */
+epicsShareFunc
+void getArrayRange(dbAddrModifier *pmod, long *pstart, long *pincr, long *pend);
+
+/** @brief Free private memory associated with an array range modifier
+ *
+ * Note this does not free 'pmod' which is always user allocated.
+ *
+ * @param pmod          Pointer to address modifier (user allocated)
+ */
+epicsShareFunc
+void deleteArrayRangeModifier(dbAddrModifier *pmod);
+
+#endif /* ARRAYRANGEMODIFIER_H */
diff --git a/modules/database/src/ioc/db/dbAccess.c b/modules/database/src/ioc/db/dbAccess.c
index bac208f..52169c0 100644
--- a/modules/database/src/ioc/db/dbAccess.c
+++ b/modules/database/src/ioc/db/dbAccess.c
@@ -40,6 +40,7 @@
 #include "callback.h"
 #include "dbAccessDefs.h"
 #include "dbAddr.h"
+#include "dbAddrModifier.h"
 #include "dbBase.h"
 #include "dbBkpt.h"
 #include "dbCommonPvt.h"
@@ -1225,6 +1226,12 @@ cleanup:
 long dbPutField(DBADDR *paddr, short dbrType,
     const void *pbuffer, long nRequest)
 {
+    return dbPutFieldModifier(paddr, dbrType, pbuffer, nRequest, NULL);
+}
+
+long dbPutFieldModifier(DBADDR *paddr, short dbrType,
+    const void *pbuffer, long nRequest, dbAddrModifier *pmod)
+{
     long        status = 0;
     long        special  = paddr->special;
     dbFldDes    *pfldDes = paddr->pfldDes;
@@ -1242,7 +1249,7 @@ long dbPutField(DBADDR *paddr, short dbrType,
         return dbPutFieldLink(paddr, dbrType, pbuffer, nRequest);
 
     dbScanLock(precord);
-    status = dbPut(paddr, dbrType, pbuffer, nRequest);
+    status = dbPutModifier(paddr, dbrType, pbuffer, nRequest, pmod);
     if (status == 0) {
         if (paddr->pfield == &precord->proc ||
             (pfldDes->process_passive &&
@@ -1296,8 +1303,13 @@ static long putAcks(DBADDR *paddr, const void *pbuffer, long nRequest,
     return 0;
 }
 
-long dbPut(DBADDR *paddr, short dbrType,
-    const void *pbuffer, long nRequest)
+long dbPut(DBADDR *paddr, short dbrType, const void *pbuffer, long nRequest)
+{
+    return dbPutModifier(paddr, dbrType, pbuffer, nRequest, NULL);
+}
+
+long dbPutModifier(DBADDR *paddr, short dbrType,
+    const void *pbuffer, long nRequest, dbAddrModifier *pmod)
 {
     dbCommon *precord = paddr->precord;
     short field_type  = paddr->field_type;
@@ -1306,6 +1318,7 @@ long dbPut(DBADDR *paddr, short dbrType,
     void *pfieldsave  = paddr->pfield;
     rset *prset = dbGetRset(paddr);
     long status = 0;
+    long available_no_elements = no_elements;
     long offset;
     dbFldDes *pfldDes;
     int isValueField;
@@ -1332,25 +1345,33 @@ long dbPut(DBADDR *paddr, short dbrType,
 
     if (paddr->pfldDes->special == SPC_DBADDR &&
         prset && prset->get_array_info) {
-        long dummy;
 
-        status = prset->get_array_info(paddr, &dummy, &offset);
+        status = prset->get_array_info(paddr, &available_no_elements, &offset);
         /* paddr->pfield may be modified */
         if (status) goto done;
-        if (no_elements < nRequest)
-            nRequest = no_elements;
-        status = dbPutConvertRoutine[dbrType][field_type](paddr, pbuffer,
-            nRequest, no_elements, offset);
-        /* update array info */
-        if (!status && prset->put_array_info)
-            status = prset->put_array_info(paddr, nRequest);
+    } else
+        offset = 0;
+
+    if (pmod && pmod->handle && pmod->pvt) {
+        status = pmod->handle(paddr, dbrType, pbuffer, nRequest,
+            pmod->pvt, offset, available_no_elements);
     } else {
-        if (nRequest < 1) {
-            recGblSetSevr(precord, LINK_ALARM, INVALID_ALARM);
-        } else {
+        if (no_elements <= 1) {
             status = dbFastPutConvertRoutine[dbrType][field_type](pbuffer,
                 paddr->pfield, paddr);
             nRequest = 1;
+        } else {
+            if (no_elements < nRequest)
+                nRequest = no_elements;
+            status = dbPutConvertRoutine[dbrType][field_type](paddr, pbuffer,
+                nRequest, no_elements, offset);
+        }
+
+        /* update array info unless writing failed */
+        if (!status &&
+            paddr->pfldDes->special == SPC_DBADDR &&
+            prset && prset->put_array_info) {
+            status = prset->put_array_info(paddr, nRequest);
         }
     }
 
diff --git a/modules/database/src/ioc/db/dbAccessDefs.h b/modules/database/src/ioc/db/dbAccessDefs.h
index f2cf091..45e9f6e 100644
--- a/modules/database/src/ioc/db/dbAccessDefs.h
+++ b/modules/database/src/ioc/db/dbAccessDefs.h
@@ -27,7 +27,9 @@
 
 #include "dbBase.h"
 #include "dbAddr.h"
+#include "dbAddrModifier.h"
 #include "recSup.h"
+#include "db_field_log.h"
 
 #ifdef __cplusplus
 extern "C" {
@@ -247,11 +249,19 @@ epicsShareFunc long dbGetField(
 epicsShareFunc long dbGet(
     struct dbAddr *,short dbrType,void *pbuffer,long *options,
     long *nRequest,void *pfl);
+
 epicsShareFunc long dbPutField(
     struct dbAddr *,short dbrType,const void *pbuffer,long nRequest);
 epicsShareFunc long dbPut(
     struct dbAddr *,short dbrType,const void *pbuffer,long nRequest);
 
+epicsShareFunc long dbPutFieldModifier(
+    struct dbAddr *,short dbrType,const void *pbuffer,long nRequest,
+    dbAddrModifier *pmod);
+epicsShareFunc long dbPutModifier(
+    struct dbAddr *,short dbrType,const void *pbuffer,long nRequest,
+    dbAddrModifier *pmod);
+
 typedef void(*SPC_ASCALLBACK)(struct dbCommon *);
 /*dbSpcAsRegisterCallback called by access security */
 epicsShareFunc void dbSpcAsRegisterCallback(SPC_ASCALLBACK func);
diff --git a/modules/database/src/ioc/db/dbAddrModifier.h b/modules/database/src/ioc/db/dbAddrModifier.h
new file mode 100644
index 0000000..dcc3c86
--- /dev/null
+++ b/modules/database/src/ioc/db/dbAddrModifier.h
@@ -0,0 +1,48 @@
+/*************************************************************************\
+* EPICS BASE is distributed subject to a Software License Agreement found
+* in file LICENSE that is included with this distribution.
+\*************************************************************************/
+#ifndef DBADDRMODIFIER_H
+#define DBADDRMODIFIER_H
+
+#include "dbAddr.h"
+
+/** @brief Generic API for address modifiers. */
+
+/** @brief Type of functions that handle an address modifier.
+ *
+ * This function should write the value in 'pbuffer' to the target 'pAddr',
+ * according to the number of requested elements 'nRequest', the requested type
+ * 'dbrType', and the address modifier private data 'pvt'. It may also take
+ * into account the actual number of elements 'available' in the target, and
+ * the 'offset', both of which usually result from a previous call to
+ * 'get_array_info'.
+ * The targeted record must be locked. Furthermore, the lock must not be given
+ * up between the call to 'get_array_info' and call to this function, otherwise
+ * 'offset' and 'available' may be out of sync.
+ *
+ * @param paddr         Target (field) address
+ * @param dbrType       Requested (element) type
+ * @param pbuffer       Data requested to be written
+ * @param nRequest      Number of elements in pbuffer
+ * @param pvt           Private modifier data
+ * @param offset        Current offset in the target field
+ * @param available     Current number of elements in the target field
+ * @return              Status
+ */
+typedef long dbHandleAddrModifier(DBADDR *paddr, short dbrType,
+    const void *pbuffer, long nRequest, void *pvt, long offset,
+    long available);
+
+/** @brief Structure of an address modifier.
+ *
+ * This will normally be allocated by user code. An address modifier
+ * implementation should supply a function to create the private data 'pvt'
+ * and initialize 'handle' with a function to handle the modifier.
+ */
+typedef struct {
+    void *pvt;                      /** @brief Private modifier data */
+    dbHandleAddrModifier *handle;   /** @brief Function to handle the modifier */
+} dbAddrModifier;
+
+#endif /* DBADDRMODIFIER_H */
diff --git a/modules/database/src/ioc/db/dbChannel.c b/modules/database/src/ioc/db/dbChannel.c
index c71d842..a9eb4e2 100644
--- a/modules/database/src/ioc/db/dbChannel.c
+++ b/modules/database/src/ioc/db/dbChannel.c
@@ -30,6 +30,7 @@
 #include "yajl_parse.h"
 
 #define epicsExportSharedSymbols
+#include "arrayRangeModifier.h"
 #include "dbAccessDefs.h"
 #include "dbBase.h"
 #include "dbChannel.h"
@@ -349,75 +350,27 @@ if (Func) { \
     if (result != parse_continue) goto failure; \
 }
 
-static long parseArrayRange(dbChannel* chan, const char *pname, const char **ppnext) {
-    epicsInt32 start = 0;
-    epicsInt32 end = -1;
-    epicsInt32 incr = 1;
-    epicsInt32 l;
-    char *pnext;
-    ptrdiff_t exist;
+static long createArrayRangeFilter(dbChannel* chan) {
+    long start, incr, end;
     chFilter *filter;
     const chFilterPlugin *plug;
     parse_result result;
-    long status = 0;
-
-    /* If no number is present, strtol() returns 0 and sets pnext=pname,
-       else pnext points to the first char after the number */
-    pname++;
-    l = strtol(pname, &pnext, 0);
-    exist = pnext - pname;
-    if (exist) start = l;
-    pname = pnext;
-    if (*pname == ']' && exist) {
-        end = start;
-        goto insertplug;
-    }
-    if (*pname != ':') {
-        status = S_dbLib_fieldNotFound;
-        goto finish;
-    }
-    pname++;
-    l = strtol(pname, &pnext, 0);
-    exist = pnext - pname;
-    pname = pnext;
-    if (*pname == ']') {
-        if (exist) end = l;
-        goto insertplug;
-    }
-    if (exist) incr = l;
-    if (*pname != ':') {
-        status = S_dbLib_fieldNotFound;
-        goto finish;
-    }
-    pname++;
-    l = strtol(pname, &pnext, 0);
-    exist = pnext - pname;
-    if (exist) end = l;
-    pname = pnext;
-    if (*pname != ']') {
-        status = S_dbLib_fieldNotFound;
-        goto finish;
-    }
-
-    insertplug:
-    pname++;
-    *ppnext = pname;
 
     plug = dbFindFilter("arr", 3);
     if (!plug) {
-        status = S_dbLib_fieldNotFound;
-        goto finish;
+        /* "arr" plugin not found, this is not an error! */
+        return 0;
     }
 
     filter = freeListCalloc(chFilterFreeList);
     if (!filter) {
-        status = S_db_noMemory;
-        goto finish;
+        return S_db_noMemory;
     }
     filter->chan = chan;
     filter->plug = plug;
     filter->puser = NULL;
 
+    getArrayRange(&chan->addrModifier, &start, &incr, &end);
     TRY(filter->plug->fif->parse_start, (filter));
     TRY(filter->plug->fif->parse_start_map, (filter));
     if (start != 0) {
@@ -438,12 +391,9 @@ static long parseArrayRange(dbChannel* chan, const char *pname, const char **ppn
     ellAdd(&chan->filters, &filter->list_node);
     return 0;
 
-    failure:
+failure:
     freeListFree(chFilterFreeList, filter);
-    status = S_dbLib_fieldNotFound;
-
-    finish:
-    return status;
+    return S_dbLib_fieldNotFound;
 }
 
 dbChannel * dbChannelCreate(const char *name)
@@ -481,6 +431,8 @@ dbChannel * dbChannelCreate(const char *name)
         goto finish;
 
     /* Handle field modifiers */
+    chan->addrModifier.pvt = NULL;
+    chan->addrModifier.handle = NULL;
     if (*pname) {
         short dbfType = paddr->field_type;
 
@@ -505,12 +457,15 @@ dbChannel * dbChannelCreate(const char *name)
             pname++;
         }
 
-        if (*pname == '[') {
-            status = parseArrayRange(chan, pname, &pname);
+        /* Try to recognize an array range expression */
+        if (parseArrayRange(&chan->addrModifier, &pname)==0) {
+            status = createArrayRangeFilter(chan);
             if (status) goto finish;
         }
 
         /* JSON may follow */
+        /* Note that chf_parse issues error messages if it fails,
+        which is why we have to check for the opening brace here. */
         if (*pname == '{') {
             status = chf_parse(chan, &pname);
             if (status) goto finish;
@@ -655,13 +610,15 @@ long dbChannelGetField(dbChannel *chan, short dbrType, void *pbuffer,
 long dbChannelPut(dbChannel *chan, short type, const void *pbuffer,
         long nRequest)
 {
-    return dbPut(&chan->addr, type, pbuffer, nRequest);
+    return dbPutModifier(&chan->addr, type, pbuffer, nRequest,
+        &chan->addrModifier);
 }
 
 long dbChannelPutField(dbChannel *chan, short type, const void *pbuffer,
         long nRequest)
 {
-    return dbPutField(&chan->addr, type, pbuffer, nRequest);
+    return dbPutFieldModifier(&chan->addr, type, pbuffer, nRequest,
+        &chan->addrModifier);
 }
 
 void dbChannelShow(dbChannel *chan, int level, const unsigned short indent)
@@ -715,6 +672,7 @@ void dbChannelDelete(dbChannel *chan)
         freeListFree(chFilterFreeList, filter);
     }
     free((char *) chan->name);
+    deleteArrayRangeModifier(&chan->addrModifier);
     freeListFree(dbChannelFreeList, chan);
 }
 
diff --git a/modules/database/src/ioc/db/dbChannel.h b/modules/database/src/ioc/db/dbChannel.h
index ec86e9e..60fbb9d 100644
--- a/modules/database/src/ioc/db/dbChannel.h
+++ b/modules/database/src/ioc/db/dbChannel.h
@@ -23,6 +23,7 @@
 #include "epicsTypes.h"
 #include "errMdef.h"
 #include "db_field_log.h"
+#include "dbAddrModifier.h"
 #include "dbEvent.h"
 #include "dbCoreAPI.h"
 
@@ -54,6 +55,8 @@ typedef struct chFilter chFilter;
 typedef struct dbChannel {
     const char *name;
     dbAddr addr;              /* address structure for record/field */
+    dbAddrModifier addrModifier;
+                              /* optional: which indices are targeted */
     long  final_no_elements;  /* final number of elements (arrays) */
     short final_field_size;   /* final size of element */
     short final_type;         /* final type of database field */
diff --git a/modules/database/src/ioc/db/dbDbLink.c b/modules/database/src/ioc/db/dbDbLink.c
index b281314..fe7c372 100644
--- a/modules/database/src/ioc/db/dbDbLink.c
+++ b/modules/database/src/ioc/db/dbDbLink.c
@@ -364,9 +364,8 @@ static long dbDbPutValue(struct link *plink, short dbrType,
     struct pv_link *ppv_link = &plink->value.pv_link;
     dbChannel *chan = linkChannel(plink);
     struct dbCommon *psrce = plink->precord;
-    DBADDR *paddr = &chan->addr;
     dbCommon *pdest = dbChannelRecord(chan);
-    long status = dbPut(paddr, dbrType, pbuffer, nRequest);
+    long status = dbChannelPut(chan, dbrType, pbuffer, nRequest);
 
     recGblInheritSevr(ppv_link->pvlMask & pvlOptMsMode, pdest, psrce->nsta,
         psrce->nsev);
diff --git a/modules/database/src/ioc/db/dbUnitTest.c b/modules/database/src/ioc/db/dbUnitTest.c
index 0b40d92..1dd4909 100644
--- a/modules/database/src/ioc/db/dbUnitTest.c
+++ b/modules/database/src/ioc/db/dbUnitTest.c
@@ -267,7 +267,7 @@ done:
 void testdbPutArrFieldOk(const char* pv, short dbrType, unsigned long count, const void *pbuf)
 {
     dbChannel *chan = dbChannelCreate(pv);
-    long status;
+    long status = 0;
 
     if(!chan || (status=dbChannelOpen(chan))) {
         testFail("Channel error (%p, %ld) : %s", chan, status, pv);
@@ -290,7 +290,7 @@ void testdbGetArrFieldEqual(const char* pv, short dbfType, long nRequest, unsign
     const long vSize = dbValueSize(dbfType);
     const long nStore = vSize * nRequest;
     long status = S_dbLib_recNotFound;
-    char *gbuf, *gstore;
+    char *gbuf, *gstore = NULL;
     const char *pbuf = pbufraw;
 
     if(!chan || (status=dbChannelOpen(chan))) {
diff --git a/modules/database/src/std/filters/arr.c b/modules/database/src/std/filters/arr.c
index ffe3fce..ed42c67 100644
--- a/modules/database/src/std/filters/arr.c
+++ b/modules/database/src/std/filters/arr.c
@@ -13,6 +13,7 @@
 
 #include <stdio.h>
 
+#include "arrayRangeModifier.h"
 #include "chfPlugin.h"
 #include "dbAccessDefs.h"
 #include "dbExtractArray.h"
@@ -74,23 +75,6 @@ static void freeArray(db_field_log *pfl)
     }
 }
 
-static long wrapArrayIndices(long *start, const long increment, long *end,
-    const long no_elements)
-{
-    if (*start < 0) *start = no_elements + *start;
-    if (*start < 0) *start = 0;
-    if (*start > no_elements) *start = no_elements;
-
-    if (*end < 0) *end = no_elements + *end;
-    if (*end < 0) *end = 0;
-    if (*end >= no_elements) *end = no_elements - 1;
-
-    if (*end - *start >= 0)
-        return 1 + (*end - *start) / increment;
-    else
-        return 0;
-}
-
 static db_field_log* filter(void* pvt, dbChannel *chan, db_field_log *pfl)
 {
     myStruct *my = (myStruct*) pvt;
diff --git a/modules/database/test/std/rec/Makefile b/modules/database/test/std/rec/Makefile
index e8c5464..71884b4 100644
--- a/modules/database/test/std/rec/Makefile
+++ b/modules/database/test/std/rec/Makefile
@@ -15,6 +15,7 @@ USR_CPPFLAGS += -DUSE_TYPED_DSET
 
 TESTLIBRARY = dbRecStdTest
 
+dbRecStdTest_SRCS += arroutRecord.c
 dbRecStdTest_SRCS += asTestLib.c
 dbRecStdTest_LIBS += dbRecStd dbCore ca Com
 
@@ -23,6 +24,7 @@ PROD_LIBS = dbRecStdTest dbRecStd dbCore ca Com
 TARGETS += $(COMMON_DIR)/recTestIoc.dbd
 DBDDEPENDS_FILES += recTestIoc.dbd$(DEP)
 recTestIoc_DBD = base.dbd
+recTestIoc_DBD += arroutRecord.dbd
 TESTFILES += $(COMMON_DIR)/recTestIoc.dbd
 
 testHarness_SRCS += recTestIoc_registerRecordDeviceDriver.cpp
@@ -166,6 +168,13 @@ testHarness_SRCS += linkFilterTest.c
 TESTFILES += ../linkFilterTest.db
 TESTS += linkFilterTest
 
+TESTPROD_HOST += addrModifierTest
+addrModifierTest_SRCS += addrModifierTest.c
+addrModifierTest_SRCS += recTestIoc_registerRecordDeviceDriver.cpp
+testHarness_SRCS += addrModifierTest.c
+TESTFILES += ../addrModifierTest.db
+TESTS += addrModifierTest
+
 # dbHeader* is only a compile test
 # no need to actually run
 TESTPROD += dbHeaderTest
@@ -199,5 +208,7 @@ endif
 
 include $(TOP)/configure/RULES
 
+arroutRecord$(DEP): $(COMMON_DIR)/arroutRecord.h
+
 rtemsTestData.c : $(TESTFILES) $(TOOLS)/epicsMakeMemFs.pl
 	$(PERL) $(TOOLS)/epicsMakeMemFs.pl $@ epicsRtemsFSImage $(TESTFILES)
diff --git a/modules/database/test/std/rec/addrModifierTest.c b/modules/database/test/std/rec/addrModifierTest.c
new file mode 100644
index 0000000..d555b91
--- /dev/null
+++ b/modules/database/test/std/rec/addrModifierTest.c
@@ -0,0 +1,247 @@
+/*************************************************************************\
+* Copyright (c) 2020 Dirk Zimoch
+* EPICS BASE is distributed subject to a Software License Agreement found
+* in file LICENSE that is included with this distribution.
+\*************************************************************************/
+
+#include <string.h>
+
+#include "dbAccess.h"
+#include "devSup.h"
+#include "alarm.h"
+#include "dbUnitTest.h"
+#include "errlog.h"
+#include "epicsThread.h"
+
+#include "longinRecord.h"
+
+#include "testMain.h"
+
+void recTestIoc_registerRecordDeviceDriver(struct dbBase *);
+
+static void startTestIoc(const char *dbfile)
+{
+    testdbPrepare();
+    testdbReadDatabase("recTestIoc.dbd", NULL, NULL);
+    recTestIoc_registerRecordDeviceDriver(pdbbase);
+    testdbReadDatabase(dbfile, NULL, NULL);
+
+    eltc(0);
+    testIocInitOk();
+    eltc(1);
+}
+
+struct pv {
+    const char *name;
+    long count;
+    double values[4];
+};
+
+static void expectProcSuccess(struct pv *pv)
+{
+    char fieldname[20];
+    testdbPutFieldOk("reset.PROC", DBF_LONG, 1);
+    testDiag("expecting success from %s", pv->name);
+    sprintf(fieldname, "%s.PROC", pv->name);
+    testdbPutFieldOk(fieldname, DBF_LONG, 1);
+    sprintf(fieldname, "%s.SEVR", pv->name);
+    testdbGetFieldEqual(fieldname, DBF_LONG, NO_ALARM);
+    sprintf(fieldname, "%s.STAT", pv->name);
+    testdbGetFieldEqual(fieldname, DBF_LONG, NO_ALARM);
+}
+
+#if 0
+static void expectProcFailure(struct pv *pv)
+{
+    char fieldname[20];
+    testdbPutFieldOk("reset.PROC", DBF_LONG, 1);
+    testDiag("expecting failure S_db_badField %#x from %s", S_db_badField, pv->name);
+    sprintf(fieldname, "%s.PROC", pv->name);
+    testdbPutFieldFail(S_db_badField, fieldname, DBF_LONG, 1);
+    sprintf(fieldname, "%s.SEVR", pv->name);
+    testdbGetFieldEqual(fieldname, DBF_LONG, INVALID_ALARM);
+    sprintf(fieldname, "%s.STAT", pv->name);
+    testdbGetFieldEqual(fieldname, DBF_LONG, LINK_ALARM);
+}
+#endif
+
+static double initial[] = {0,1,2,3,4,5,6,7,8,9};
+static double buf[10];
+
+static void changeRange(struct pv *pv, long start, long incr, long end)
+{
+    char linkstring[60];
+    char pvstring[10];
+
+    if (incr)
+        sprintf(linkstring, "tgt.[%ld:%ld:%ld]", start, incr, end);
+    else if (end)
+        sprintf(linkstring, "tgt.[%ld:%ld]", start, end);
+    else
+        sprintf(linkstring, "tgt.[%ld]", start);
+    testDiag("modifying %s.OUT link: %s", pv->name, linkstring);
+    sprintf(pvstring, "%s.OUT", pv->name);
+    testdbPutFieldOk(pvstring, DBF_STRING, linkstring);
+}
+
+static void expectRange(double *values, long start, long incr, long end)
+{
+    int i,j;
+    if (!incr)
+        incr = 1;
+    for (i=0; i<10; i++)
+        buf[i] = initial[i];
+    for (i=0, j=start; j<=end; i++, j+=incr)
+        buf[j] = values[i];
+    testdbGetFieldEqual("tgt.NORD", DBF_LONG, 8);
+    testdbGetFieldEqual("tgt.SEVR", DBF_LONG, NO_ALARM);
+    testdbGetFieldEqual("tgt.STAT", DBF_LONG, NO_ALARM);
+    testdbGetArrFieldEqual("tgt.VAL", DBF_DOUBLE, 10, 8, buf);
+}
+
+#if 0
+static void expectEmptyArray(void)
+{
+    /* empty arrays are now allowed at the moment */
+    testDiag("expecting empty array");
+    testdbGetFieldEqual("tgt.NORD", DBF_LONG, 0);
+}
+#endif
+
+struct pv ao_pv = {"ao",1,{20,0,0,0}};
+struct pv src_pv = {"src",4,{30,40,50,60}};
+struct pv *ao = &ao_pv;
+struct pv *src = &src_pv;
+
+static double pini_values[] = {20,30,40,50};
+
+#define expectEmptyRange() expectRange(0,0,0,-1)
+
+MAIN(addrModifierTest)
+{
+    testPlan(205);
+    startTestIoc("addrModifierTest.db");
+
+    testdbGetFieldEqual("src.NORD", DBF_LONG, 4);
+    testDiag("PINI");
+    expectRange(pini_values,2,1,5);
+
+    testDiag("after processing");
+    testdbPutFieldOk("ao.PROC", DBF_LONG, 1);
+    testdbPutFieldOk("src.PROC", DBF_LONG, 1);
+    expectRange(pini_values,2,1,5);
+
+    testDiag("after processing target record");
+    testdbPutFieldOk("tgt.PROC", DBF_LONG, 1);
+    expectRange(pini_values,2,1,5);
+
+    testDiag("modify range");
+
+    changeRange(ao,3,0,0);
+    expectProcSuccess(ao);
+    expectRange(ao->values,3,1,3);
+
+    changeRange(src,4,0,7);
+    expectProcSuccess(src);
+    expectRange(src->values,4,1,7);
+
+    testDiag("put more than available");
+
+    changeRange(ao,3,0,6);
+    expectProcSuccess(ao);
+    expectRange(ao->values,3,1,3); /* clipped range */
+
+    changeRange(src,3,0,9);
+    expectProcSuccess(src);
+    expectRange(src->values,3,1,6); /* clipped range */
+
+    testDiag("backward range");
+
+    changeRange(ao,5,0,3);
+    expectProcSuccess(ao);
+    expectEmptyRange();
+
+    changeRange(src,5,0,3);
+    expectProcSuccess(src);
+    expectEmptyRange();
+
+    testDiag("step 2");
+
+    changeRange(ao,1,2,6);
+    expectProcSuccess(ao);
+    expectRange(ao->values,1,1,1); /* clipped range */
+
+    changeRange(src,1,2,6);
+    expectProcSuccess(src);
+    expectRange(src->values,1,2,5);
+
+    testDiag("range start beyond tgt.NORD");
+
+    changeRange(ao,8,0,0);
+    expectProcSuccess(ao);
+    expectEmptyRange();
+
+    changeRange(src,8,0,9);
+    expectProcSuccess(src);
+    expectEmptyRange();
+
+    testDiag("range end beyond tgt.NORD");
+
+    changeRange(ao,3,0,9);
+    expectProcSuccess(ao);
+    expectRange(ao->values,3,1,3); /* clipped range */
+
+    changeRange(src,3,0,9);
+    expectProcSuccess(src);
+    expectRange(src->values,3,1,6); /* clipped range */
+
+    testDiag("range start beyond tgt.NELM");
+
+    changeRange(ao,11,0,12);
+    expectProcSuccess(ao);
+    expectEmptyRange();
+
+    changeRange(src,11,0,12);
+    expectProcSuccess(src);
+    expectEmptyRange();
+
+    testDiag("range end beyond tgt.NELM");
+
+    changeRange(src,4,0,12);
+    expectProcSuccess(src);
+    expectRange(src->values,4,1,7); /* clipped range */
+
+    testDiag("single value beyond tgt.NORD");
+
+    changeRange(ao,8,0,0);
+    expectProcSuccess(ao);
+    expectEmptyRange();
+
+    changeRange(src,8,0,0);
+    expectProcSuccess(src);
+    expectEmptyRange();
+
+    testDiag("single value");
+
+    changeRange(ao,5,0,0);
+    expectProcSuccess(ao);
+    expectRange(ao->values,5,1,5);
+
+    changeRange(src,5,0,0);
+    expectProcSuccess(src);
+    expectRange(src->values,5,1,5);
+
+    testDiag("single values beyond tgt.NELM");
+
+    changeRange(ao,12,0,0);
+    expectProcSuccess(ao);
+    expectEmptyRange();
+
+    changeRange(src,12,0,0);
+    expectProcSuccess(src);
+    expectEmptyRange();
+
+    testIocShutdownOk();
+    testdbCleanup();
+    return testDone();
+}
diff --git a/modules/database/test/std/rec/addrModifierTest.db b/modules/database/test/std/rec/addrModifierTest.db
new file mode 100644
index 0000000..ed28a80
--- /dev/null
+++ b/modules/database/test/std/rec/addrModifierTest.db
@@ -0,0 +1,24 @@
+record(arrout, "tgt") {
+    field(NELM, "10")
+    field(FTVL, "SHORT")
+    field(DOL, [0, 1, 2, 3, 4, 5, 6, 7])
+    field(PINI, "YES")
+}
+record(ao, "ao") {
+    field(OUT, "tgt.[2]")
+    field(DOL, 20)
+    field(PINI, "YES")
+}
+record(arrout, "src") {
+    field(NELM, "5")
+    field(FTVL, "DOUBLE")
+    field(DOL, [30.0, 40.0, 50.0, 60.0])
+    field(OUT, "tgt.[3:5]")
+    field(PINI, "YES")
+}
+record(arrout, "reset") {
+    field(NELM, "10")
+    field(FTVL, "SHORT")
+    field(DOL, [0, 1, 2, 3, 4, 5, 6, 7])
+    field(OUT, "tgt PP")
+}
diff --git a/modules/database/test/std/rec/arroutRecord.c b/modules/database/test/std/rec/arroutRecord.c
new file mode 100644
index 0000000..61c7680
--- /dev/null
+++ b/modules/database/test/std/rec/arroutRecord.c
@@ -0,0 +1,169 @@
+/*************************************************************************\
+* Copyright (c) 2010 Brookhaven National Laboratory.
+* Copyright (c) 2010 Helmholtz-Zentrum Berlin
+*     fuer Materialien und Energie GmbH.
+* Copyright (c) 2008 UChicago Argonne LLC, as Operator of Argonne
+*     National Laboratory.
+* Copyright (c) 2002 The Regents of the University of California, as
+*     Operator of Los Alamos National Laboratory.
+* EPICS BASE is distributed subject to a Software License Agreement found
+* in file LICENSE that is included with this distribution.
+\*************************************************************************/
+
+/* arroutRecord.c - minimal array output record for test purposes: no processing */
+
+/* adapted from: arrRecord.c
+ *
+ *  Author: Ralph Lange <Ralph.Lange at bessy.de>
+ *
+ * vaguely implemented like parts of recWaveform.c by Bob Dalesio
+ *
+ */
+
+#include <stdio.h>
+
+#include "alarm.h"
+#include "epicsPrint.h"
+#include "dbAccess.h"
+#include "dbEvent.h"
+#include "dbFldTypes.h"
+#include "recSup.h"
+#include "recGbl.h"
+#include "cantProceed.h"
+#define GEN_SIZE_OFFSET
+#include "arroutRecord.h"
+#undef  GEN_SIZE_OFFSET
+#include "epicsExport.h"
+
+/* Create RSET - Record Support Entry Table*/
+#define report NULL
+#define initialize NULL
+static long init_record(struct dbCommon *, int);
+static long process(struct dbCommon *);
+#define special NULL
+#define get_value NULL
+static long cvt_dbaddr(DBADDR *);
+static long get_array_info(DBADDR *, long *, long *);
+static long put_array_info(DBADDR *, long);
+#define get_units NULL
+#define get_precision NULL
+#define get_enum_str NULL
+#define get_enum_strs NULL
+#define put_enum_str NULL
+#define get_graphic_double NULL
+#define get_control_double NULL
+#define get_alarm_double NULL
+
+rset arroutRSET = {
+        RSETNUMBER,
+        report,
+        initialize,
+        init_record,
+        process,
+        special,
+        get_value,
+        cvt_dbaddr,
+        get_array_info,
+        put_array_info,
+        get_units,
+        get_precision,
+        get_enum_str,
+        get_enum_strs,
+        put_enum_str,
+        get_graphic_double,
+        get_control_double,
+        get_alarm_double
+};
+epicsExportAddress(rset, arroutRSET);
+
+static long init_record(struct dbCommon *pcommon, int pass)
+{
+    struct arroutRecord *prec = (struct arroutRecord *)pcommon;
+
+    if (pass == 0) {
+        if (prec->nelm <= 0)
+            prec->nelm = 1;
+        if (prec->ftvl > DBF_ENUM)
+            prec->ftvl = DBF_UCHAR;
+        prec->val = callocMustSucceed(prec->nelm, dbValueSize(prec->ftvl),
+            "arr calloc failed");
+        return 0;
+    } else {
+        /* copied from devWfSoft.c */
+        long nelm = prec->nelm;
+        long status = dbLoadLinkArray(&prec->dol, prec->ftvl, prec->val, &nelm);
+
+        if (!status && nelm > 0) {
+            prec->nord = nelm;
+            prec->udf = FALSE;
+        }
+        else
+            prec->nord = 0;
+        return status;
+    }
+}
+
+static long process(struct dbCommon *pcommon)
+{
+    struct arroutRecord *prec = (struct arroutRecord *)pcommon;
+    long status = 0;
+
+    prec->pact = TRUE;
+    /* read DOL */
+    if (!dbLinkIsConstant(&prec->dol)) {
+        long nReq = prec->nelm;
+
+        status = dbGetLink(&prec->dol, prec->ftvl, prec->val, 0, &nReq);
+        if (status) {
+            recGblSetSevr(prec, LINK_ALARM, INVALID_ALARM);
+        } else {
+            prec->nord = nReq;
+        }
+    }
+    /* soft "device support": write OUT */
+    status = dbPutLink(&prec->out, prec->ftvl, prec->val, prec->nord);
+    if (status) {
+        recGblSetSevr(prec, LINK_ALARM, INVALID_ALARM);
+    }
+    recGblGetTimeStamp(prec);
+    recGblResetAlarms(prec);
+    recGblFwdLink(prec);
+    prec->pact = FALSE;
+    return status;
+}
+
+static long cvt_dbaddr(DBADDR *paddr)
+{
+    arroutRecord *prec = (arroutRecord *) paddr->precord;
+    int fieldIndex = dbGetFieldIndex(paddr);
+
+    if (fieldIndex == arroutRecordVAL) {
+        paddr->pfield = prec->val;
+        paddr->no_elements = prec->nelm;
+        paddr->field_type = prec->ftvl;
+        paddr->field_size = dbValueSize(prec->ftvl);
+        paddr->dbr_field_type = prec->ftvl;
+    }
+    return 0;
+}
+
+static long get_array_info(DBADDR *paddr, long *no_elements, long *offset)
+{
+    arroutRecord *prec = (arroutRecord *) paddr->precord;
+
+    *no_elements = prec->nord;
+    *offset = prec->off;
+
+    return 0;
+}
+
+static long put_array_info(DBADDR *paddr, long nNew)
+{
+    arroutRecord *prec = (arroutRecord *) paddr->precord;
+
+    prec->nord = nNew;
+    if (prec->nord > prec->nelm)
+        prec->nord = prec->nelm;
+
+    return 0;
+}
diff --git a/modules/database/test/std/rec/arroutRecord.dbd b/modules/database/test/std/rec/arroutRecord.dbd
new file mode 100644
index 0000000..2c99e64
--- /dev/null
+++ b/modules/database/test/std/rec/arroutRecord.dbd
@@ -0,0 +1,35 @@
+include "menuGlobal.dbd"
+include "menuConvert.dbd"
+include "menuScan.dbd"
+recordtype(arrout) {
+  include "dbCommon.dbd"
+  field(VAL, DBF_NOACCESS) {
+    prompt("Value")
+    special(SPC_DBADDR)
+    pp(TRUE)
+    extra("void *val")
+  }
+  field(OUT, DBF_OUTLINK) {
+    prompt("Output Link")
+  }
+  field(NELM, DBF_ULONG) {
+    prompt("Number of Elements")
+    special(SPC_NOMOD)
+    initial("1")
+  }
+  field(FTVL, DBF_MENU) {
+    prompt("Field Type of Value")
+    special(SPC_NOMOD)
+    menu(menuFtype)
+  }
+  field(NORD, DBF_ULONG) {
+    prompt("Number elements read")
+    special(SPC_NOMOD)
+  }
+  field(OFF, DBF_ULONG) {
+    prompt("Offset into array")
+  }
+  field(DOL, DBF_INLINK) {
+    prompt("Desired Output Link")
+  }
+}

Replies:
Re: [Merge] ~bfrk/epics-base:address-modifiers into epics-base:7.0 Ben Franksen via Core-talk

Navigate by Date:
Prev: [Merge] ~bfrk/epics-base:address-modifiers into epics-base:7.0 Ben Franksen via Core-talk
Next: Re: [Merge] ~bfrk/epics-base:address-modifiers into epics-base:7.0 Ben Franksen via Core-talk
Index: 2002  2003  2004  2005  2006  2007  2008  2009  2010  2011  2012  2013  2014  2015  2016  2017  2018  2019  2020  <20212022  2023  2024 
Navigate by Thread:
Prev: [Merge] ~bfrk/epics-base:address-modifiers into epics-base:7.0 Ben Franksen via Core-talk
Next: Re: [Merge] ~bfrk/epics-base:address-modifiers into epics-base:7.0 Ben Franksen via Core-talk
Index: 2002  2003  2004  2005  2006  2007  2008  2009  2010  2011  2012  2013  2014  2015  2016  2017  2018  2019  2020  <20212022  2023  2024 
ANJ, 01 Apr 2021 Valid HTML 4.01! · Home · News · About · Base · Modules · Extensions · Distributions · Download ·
· Search · EPICS V4 · IRMIS · Talk · Bugs · Documents · Links · Licensing ·