[Tinyos-2-commits] CVS: tinyos-2.x/apps/tosthreads/apps/SerialCompress Makefile, NONE, 1.1 SerialCompressAppC.nc, NONE, 1.1 SerialCompressP.nc, NONE, 1.1 lz.c, NONE, 1.1 lz.h, NONE, 1.1 volumes-at45db.xml, NONE, 1.1 volumes-stm25p.xml, NONE, 1.1

Kevin Klues klueska at users.sourceforge.net
Thu Jun 12 08:11:40 PDT 2008


Update of /cvsroot/tinyos/tinyos-2.x/apps/tosthreads/apps/SerialCompress
In directory sc8-pr-cvs10.sourceforge.net:/tmp/cvs-serv2426/tosthreads/apps/SerialCompress

Added Files:
	Makefile SerialCompressAppC.nc SerialCompressP.nc lz.c lz.h 
	volumes-at45db.xml volumes-stm25p.xml 
Log Message:
initial checkin of tosthreads related apps

--- NEW FILE: Makefile ---
COMPONENT=SerialCompressAppC

CFLAGS += -I../bcl-1.2.0/src

include $(MAKERULES)

--- NEW FILE: SerialCompressAppC.nc ---
#include "StorageVolumes.h"

configuration SerialCompressAppC {}

implementation
{
  components MainC,
             LedsC,
             SerialActiveMessageC,
             new SerialAMReceiverC(0x25),
             new SerialAMSenderC(0x25),
             new LogStorageC(VOLUME_SENSORLOG, TRUE),
             SerialCompressP;
             
  SerialCompressP.Boot -> MainC;
  SerialCompressP.Leds -> LedsC;
  SerialCompressP.LogRead -> LogStorageC;
  SerialCompressP.LogWrite -> LogStorageC;
  SerialCompressP.SerialSplitControl -> SerialActiveMessageC;
  SerialCompressP.AMSend -> SerialAMSenderC;
  SerialCompressP.Receive -> SerialAMReceiverC;
  SerialCompressP.AMPacket -> SerialActiveMessageC;
}

--- NEW FILE: SerialCompressP.nc ---
#include "lz.c"
#include "lz.h"

#define NUM_RECORDS_TO_COMPRESS 50

module SerialCompressP {
  uses {
    interface Boot;
    interface Leds;
    interface LogRead;
    interface LogWrite;
    interface SplitControl as SerialSplitControl;
    interface Receive;
    interface AMSend;
    interface AMPacket;
  }
}

implementation {
  typedef nx_struct serial_data {
    nx_uint32_t pktNum;
    nx_uint8_t data[0];
  } serial_data_t;

  serial_data_t *writeEntry;
  bool isBusy_writeEntry = FALSE;
  serial_data_t *readEntry;
  bool isBusy_readEntry = FALSE;

  uint16_t numRecords = 0;  
  uint16_t MAX_SERIAL_DATA_LENGTH = TOSH_DATA_LENGTH - sizeof(serial_data_t);
  uint32_t expectedPktNum = 0;
  uint32_t pktLoss = 0;
  //uint8_t in[NUM_RECORDS_TO_COMPRESS * MAX_SERIAL_DATA_LENGTH];
  //uint8_t out[((NUM_RECORDS_TO_COMPRESS * MAX_SERIAL_DATA_LENGTH * 257 - 1) / 256) + 1 + 1];
  uint8_t *in;
  uint8_t *out;

  message_t mesg;

  event void AMSend.sendDone(message_t *msg, error_t error) {}

  event void Boot.booted()
  {
    call Leds.set(7);
    writeEntry = (serial_data_t *)malloc(TOSH_DATA_LENGTH);
    readEntry = (serial_data_t *)malloc(TOSH_DATA_LENGTH);
    in = malloc(NUM_RECORDS_TO_COMPRESS * MAX_SERIAL_DATA_LENGTH);
    out = malloc(NUM_RECORDS_TO_COMPRESS * MAX_SERIAL_DATA_LENGTH * 2);
    while (call LogWrite.erase() != SUCCESS) {}
  }
  
  event message_t* Receive.receive(message_t *msg, void *msg_payload, uint8_t len)
  {
    if (isBusy_writeEntry == FALSE) {
      serial_data_t *payload = (serial_data_t *)msg_payload;
      
      writeEntry->pktNum = payload->pktNum;
      memcpy(writeEntry->data, payload->data, MAX_SERIAL_DATA_LENGTH);
      
      if (payload->pktNum == 0xFFFFFFFF) {
        uint8_t *p = (uint8_t *) call AMSend.getPayload(&mesg, 4);
        p[0] = (pktLoss >> 24) & 0xFF;
        p[1] = (pktLoss >> 16) & 0xFF;
        p[2] = (pktLoss >> 8) & 0xFF;
        p[3] = pktLoss & 0xFF;
        call AMSend.send(AM_BROADCAST_ADDR, &mesg, 4);
      } else if (payload->pktNum != expectedPktNum) {
        pktLoss += payload->pktNum - expectedPktNum;
        call Leds.led1Toggle();
      }
      expectedPktNum = payload->pktNum + 1;
      
      if (call LogWrite.append(writeEntry, TOSH_DATA_LENGTH) == SUCCESS) {
        isBusy_writeEntry = TRUE;
      }
    }
    
    return msg;
  }

  task void processLog()
  {
    if (call LogRead.currentOffset() < call LogWrite.currentOffset()) {
      if (isBusy_readEntry == FALSE) {
        if (call LogRead.read(readEntry, TOSH_DATA_LENGTH) == SUCCESS) {
          isBusy_readEntry = TRUE;
        } else {
          post processLog();
        }
      }
    }
  }
  
  task void readDoneTask()
  {
    memcpy(&(in[numRecords * MAX_SERIAL_DATA_LENGTH]), readEntry->data, MAX_SERIAL_DATA_LENGTH);
    numRecords++;
      
    if (numRecords == NUM_RECORDS_TO_COMPRESS) {
      call Leds.led2Toggle();
      LZ_Compress(in, out, NUM_RECORDS_TO_COMPRESS * MAX_SERIAL_DATA_LENGTH);
      numRecords = 0;
    }
      
    isBusy_readEntry = FALSE;
    post processLog();
  }
  
  event void LogRead.readDone(void* buf, storage_len_t len, error_t error)
  {
    if (error == SUCCESS) {
      post readDoneTask();
    } else {
      isBusy_readEntry = FALSE;
      post processLog();
    }
  }
  
  event void LogWrite.appendDone(void* buf, storage_len_t len, bool recordsLost, error_t error)
  {
    isBusy_writeEntry = FALSE;
    
    if (error == SUCCESS) {
      post processLog(); 
    }
  }
  
  event void LogWrite.eraseDone(error_t error)
  {
    if (error == SUCCESS) {
      while (call SerialSplitControl.start() != SUCCESS) {}
    } else {
      while (call LogWrite.erase() != SUCCESS) {}      
    }
  }

  event void LogRead.seekDone(error_t error) {}
  
  event void SerialSplitControl.startDone(error_t error)
  {
    if (error == SUCCESS) {
      call Leds.set(0);
    } else {
      while (call SerialSplitControl.start() != SUCCESS) {}
    }
  }
  
  event void SerialSplitControl.stopDone(error_t error) {}
  event void LogWrite.syncDone(error_t error) {}
}

--- NEW FILE: lz.c ---
/*************************************************************************
* Name:        lz.c
* Author:      Marcus Geelnard
* Description: LZ77 coder/decoder implementation.
* Reentrant:   Yes
*
* The LZ77 compression scheme is a substitutional compression scheme
* proposed by Abraham Lempel and Jakob Ziv in 1977. It is very simple in
* its design, and uses no fancy bit level compression.
*
* This is my first attempt at an implementation of a LZ77 code/decoder.
*
* The principle of the LZ77 compression algorithm is to store repeated
* occurrences of strings as references to previous occurrences of the same
* string. The point is that the reference consumes less space than the
* string itself, provided that the string is long enough (in this
* implementation, the string has to be at least 4 bytes long, since the
* minimum coded reference is 3 bytes long). Also note that the term
* "string" refers to any kind of byte sequence (it does not have to be
* an ASCII string, for instance).
*
* The coder uses a brute force approach to finding string matches in the
* history buffer (or "sliding window", if you wish), which is very, very
* slow. I recon the complexity is somewhere between O(n^2) and O(n^3),
* depending on the input data.
*
* There is also a faster implementation that uses a large working buffer
* in which a "jump table" is stored, which is used to quickly find
* possible string matches (see the source code for LZ_CompressFast() for
* more information). The faster method is an order of magnitude faster,
* but still quite slow compared to other compression methods.
*
* The upside is that decompression is very fast, and the compression ratio
* is often very good.
*
* The reference to a string is coded as a (length,offset) pair, where the
* length indicates the length of the string, and the offset gives the
* offset from the current data position. To distinguish between string
* references and literal strings (uncompressed bytes), a string reference
* is preceded by a marker byte, which is chosen as the least common byte
* symbol in the input data stream (this marker byte is stored in the
* output stream as the first byte).
*
* Occurrences of the marker byte in the stream are encoded as the marker
* byte followed by a zero byte, which means that occurrences of the marker
* byte have to be coded with two bytes.
*
* The lengths and offsets are coded in a variable length fashion, allowing
* values of any magnitude (up to 4294967295 in this implementation).
*
* With this compression scheme, the worst case compression result is
* (257/256)*insize + 1.
*
*-------------------------------------------------------------------------
* Copyright (c) 2003-2006 Marcus Geelnard
*
* This software is provided 'as-is', without any express or implied
* warranty. In no event will the authors be held liable for any damages
* arising from the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
*
* 1. The origin of this software must not be misrepresented; you must not
*    claim that you wrote the original software. If you use this software
*    in a product, an acknowledgment in the product documentation would
*    be appreciated but is not required.
*
* 2. Altered source versions must be plainly marked as such, and must not
*    be misrepresented as being the original software.
*
* 3. This notice may not be removed or altered from any source
*    distribution.
*
* Marcus Geelnard
* marcus.geelnard at home.se
*************************************************************************/


/*************************************************************************
* Constants used for LZ77 coding
*************************************************************************/

/* Maximum offset (can be any size < 2^31). Lower values give faster
   compression, while higher values gives better compression. The default
   value of 100000 is quite high. Experiment to see what works best for
   you. */
#define LZ_MAX_OFFSET 100000



/*************************************************************************
*                           INTERNAL FUNCTIONS                           *
*************************************************************************/


/*************************************************************************
* _LZ_StringCompare() - Return maximum length string match.
*************************************************************************/

static uint32_t _LZ_StringCompare( unsigned char * str1,
  unsigned char * str2, uint32_t minlen, uint32_t maxlen )
{
    uint32_t len;

    for( len = minlen; (len < maxlen) && (str1[len] == str2[len]); ++ len );

    return len;
}


/*************************************************************************
* _LZ_WriteVarSize() - Write uint32_teger with variable number of
* bytes depending on value.
*************************************************************************/

static int _LZ_WriteVarSize( uint32_t x, unsigned char * buf )
{
    uint32_t y;
    int num_bytes, i, b;

    /* Determine number of bytes needed to store the number x */
    y = x >> 3;
    for( num_bytes = 5; num_bytes >= 2; -- num_bytes )
    {
        if( y & 0xfe000000 ) break;
        y <<= 7;
    }

    /* Write all bytes, seven bits in each, with 8:th bit set for all */
    /* but the last byte. */
    for( i = num_bytes-1; i >= 0; -- i )
    {
        b = (x >> (i*7)) & 0x0000007f;
        if( i > 0 )
        {
            b |= 0x00000080;
        }
        *buf ++ = (unsigned char) b;
    }

    /* Return number of bytes written */
    return num_bytes;
}


/*************************************************************************
* _LZ_ReadVarSize() - Read uint32_teger with variable number of
* bytes depending on value.
*************************************************************************/

static int _LZ_ReadVarSize( uint32_t * x, unsigned char * buf )
{
    uint32_t y, b, num_bytes;

    /* Read complete value (stop when byte contains zero in 8:th bit) */
    y = 0;
    num_bytes = 0;
    do
    {
        b = (uint32_t) (*buf ++);
        y = (y << 7) | (b & 0x0000007f);
        ++ num_bytes;
    }
    while( b & 0x00000080 );

    /* Store value in x */
    *x = y;

    /* Return number of bytes read */
    return num_bytes;
}



/*************************************************************************
*                            PUBLIC FUNCTIONS                            *
*************************************************************************/


/*************************************************************************
* LZ_Compress() - Compress a block of data using an LZ77 coder.
*  in     - Input (uncompressed) buffer.
*  out    - Output (compressed) buffer. This buffer must be 0.4% larger
*           than the input buffer, plus one byte.
*  insize - Number of input bytes.
* The function returns the size of the compressed data.
*************************************************************************/

int LZ_Compress( unsigned char *in, unsigned char *out,
    uint32_t insize )
{
    unsigned char marker, symbol;
    uint32_t  inpos, outpos, bytesleft, i;
    uint32_t  maxoffset, offset, bestoffset;
    uint32_t  maxlength, length, bestlength;
    uint32_t  histogram[ 256 ];
    unsigned char *ptr1, *ptr2;

    /* Do we have anything to compress? */
    if( insize < 1 )
    {
        return 0;
    }

    /* Create histogram */
    for( i = 0; i < 256; ++ i )
    {
        histogram[ i ] = 0;
    }
    for( i = 0; i < insize; ++ i )
    {
        ++ histogram[ in[ i ] ];
    }

    /* Find the least common byte, and use it as the marker symbol */
    marker = 0;
    for( i = 1; i < 256; ++ i )
    {
        if( histogram[ i ] < histogram[ marker ] )
        {
            marker = i;
        }
    }

    /* Remember the marker symbol for the decoder */
    out[ 0 ] = marker;

    /* Start of compression */
    inpos = 0;
    outpos = 1;

    /* Main compression loop */
    bytesleft = insize;
    do
    {
        /* Determine most distant position */
        if( inpos > LZ_MAX_OFFSET ) {
          maxoffset = LZ_MAX_OFFSET;
        }
        else {
        maxoffset = inpos;
        }

        /* Get pointer to current position */
        ptr1 = &in[ inpos ];

        /* Search history window for maximum length string match */
        bestlength = 3;
        bestoffset = 0;
        for( offset = 3; offset <= maxoffset; ++ offset )
        {
            /* Get pointer to candidate string */
            ptr2 = &ptr1[ -(int)offset ];

            /* Quickly determine if this is a candidate (for speed) */
            if( (ptr1[ 0 ] == ptr2[ 0 ]) &&
                (ptr1[ bestlength ] == ptr2[ bestlength ]) )
            {
                /* Determine maximum length for this offset */
                maxlength = (bytesleft < offset ? bytesleft : offset);

                /* Count maximum length match at this offset */
                length = _LZ_StringCompare( ptr1, ptr2, 0, maxlength );

                /* Better match than any previous match? */
                if( length > bestlength )
                {
                    bestlength = length;
                    bestoffset = offset;
                }
            }
        }

        /* Was there a good enough match? */
        if( (bestlength >= 8) ||
            ((bestlength == 4) && (bestoffset <= 0x0000007f)) ||
            ((bestlength == 5) && (bestoffset <= 0x00003fff)) ||
            ((bestlength == 6) && (bestoffset <= 0x001fffff)) ||
            ((bestlength == 7) && (bestoffset <= 0x0fffffff)) )
        {
            out[ outpos ++ ] = (unsigned char) marker;
            outpos += _LZ_WriteVarSize( bestlength, &out[ outpos ] );
            outpos += _LZ_WriteVarSize( bestoffset, &out[ outpos ] );
            inpos += bestlength;
            bytesleft -= bestlength;
        }
        else
        {
            /* Output single byte (or two bytes if marker byte) */
            symbol = in[ inpos ++ ];
            out[ outpos ++ ] = symbol;
            if( symbol == marker )
            {
                out[ outpos ++ ] = 0;
            }
            -- bytesleft;
        }
    }
    while( bytesleft > 3 );

    /* Dump remaining bytes, if any */
    while( inpos < insize )
    {
        if( in[ inpos ] == marker )
        {
            out[ outpos ++ ] = marker;
            out[ outpos ++ ] = 0;
        }
        else
        {
            out[ outpos ++ ] = in[ inpos ];
        }
        ++ inpos;
    }

    return outpos;
}


/*************************************************************************
* LZ_CompressFast() - Compress a block of data using an LZ77 coder.
*  in     - Input (uncompressed) buffer.
*  out    - Output (compressed) buffer. This buffer must be 0.4% larger
*           than the input buffer, plus one byte.
*  insize - Number of input bytes.
*  work   - Pointer to a temporary buffer (internal working buffer), which
*           must be able to hold (insize+65536) uint32_tegers.
* The function returns the size of the compressed data.
*************************************************************************/

int LZ_CompressFast( unsigned char *in, unsigned char *out,
    uint32_t insize, uint32_t *work )
{
    unsigned char marker, symbol;
    uint32_t  inpos, outpos, bytesleft, i, index, symbols;
    uint32_t  offset, bestoffset;
    uint32_t  maxlength, length, bestlength;
    uint32_t  histogram[ 256 ], *lastindex, *jumptable;
    unsigned char *ptr1, *ptr2;

    /* Do we have anything to compress? */
    if( insize < 1 )
    {
        return 0;
    }

    /* Assign arrays to the working area */
    lastindex = work;
    jumptable = &work[ 65536 ];

    /* Build a "jump table". Here is how the jump table works:
       jumptable[i] points to the nearest previous occurrence of the same
       symbol pair as in[i]:in[i+1], so in[i] == in[jumptable[i]] and
       in[i+1] == in[jumptable[i]+1], and so on... Following the jump table
       gives a dramatic boost for the string search'n'match loop compared
       to doing a brute force search. The jump table is built in O(n) time,
       so it is a cheap operation in terms of time, but it is expensice in
       terms of memory consumption. */
    for( i = 0; i < 65536; ++ i )
    {
        lastindex[ i ] = 0xffffffff;
    }
    for( i = 0; i < insize-1; ++ i )
    {
        symbols = (((uint32_t)in[i]) << 8) | ((uint32_t)in[i+1]);
        index = lastindex[ symbols ];
        lastindex[ symbols ] = i;
        jumptable[ i ] = index;
    }
    jumptable[ insize-1 ] = 0xffffffff;

    /* Create histogram */
    for( i = 0; i < 256; ++ i )
    {
        histogram[ i ] = 0;
    }
    for( i = 0; i < insize; ++ i )
    {
        ++ histogram[ in[ i ] ];
    }

    /* Find the least common byte, and use it as the marker symbol */
    marker = 0;
    for( i = 1; i < 256; ++ i )
    {
        if( histogram[ i ] < histogram[ marker ] )
        {
            marker = i;
        }
    }

    /* Remember the marker symbol for the decoder */
    out[ 0 ] = marker;

    /* Start of compression */
    inpos = 0;
    outpos = 1;

    /* Main compression loop */
    bytesleft = insize;
    do
    {
        /* Get pointer to current position */
        ptr1 = &in[ inpos ];

        /* Search history window for maximum length string match */
        bestlength = 3;
        bestoffset = 0;
        index = jumptable[ inpos ];
        while( (index != 0xffffffff) && ((inpos - index) < LZ_MAX_OFFSET) )
        {
            /* Get pointer to candidate string */
            ptr2 = &in[ index ];

            /* Quickly determine if this is a candidate (for speed) */
            if( ptr2[ bestlength ] == ptr1[ bestlength ] )
            {
                /* Determine maximum length for this offset */
                offset = inpos - index;
                maxlength = (bytesleft < offset ? bytesleft : offset);

                /* Count maximum length match at this offset */
                length = _LZ_StringCompare( ptr1, ptr2, 2, maxlength );

                /* Better match than any previous match? */
                if( length > bestlength )
                {
                    bestlength = length;
                    bestoffset = offset;
                }
            }

            /* Get next possible index from jump table */
            index = jumptable[ index ];
        }

        /* Was there a good enough match? */
        if( (bestlength >= 8) ||
            ((bestlength == 4) && (bestoffset <= 0x0000007f)) ||
            ((bestlength == 5) && (bestoffset <= 0x00003fff)) ||
            ((bestlength == 6) && (bestoffset <= 0x001fffff)) ||
            ((bestlength == 7) && (bestoffset <= 0x0fffffff)) )
        {
            out[ outpos ++ ] = (unsigned char) marker;
            outpos += _LZ_WriteVarSize( bestlength, &out[ outpos ] );
            outpos += _LZ_WriteVarSize( bestoffset, &out[ outpos ] );
            inpos += bestlength;
            bytesleft -= bestlength;
        }
        else
        {
            /* Output single byte (or two bytes if marker byte) */
            symbol = in[ inpos ++ ];
            out[ outpos ++ ] = symbol;
            if( symbol == marker )
            {
                out[ outpos ++ ] = 0;
            }
            -- bytesleft;
        }
    }
    while( bytesleft > 3 );

    /* Dump remaining bytes, if any */
    while( inpos < insize )
    {
        if( in[ inpos ] == marker )
        {
            out[ outpos ++ ] = marker;
            out[ outpos ++ ] = 0;
        }
        else
        {
            out[ outpos ++ ] = in[ inpos ];
        }
        ++ inpos;
    }

    return outpos;
}


/*************************************************************************
* LZ_Uncompress() - Uncompress a block of data using an LZ77 decoder.
*  in      - Input (compressed) buffer.
*  out     - Output (uncompressed) buffer. This buffer must be large
*            enough to hold the uncompressed data.
*  insize  - Number of input bytes.
*************************************************************************/

void LZ_Uncompress( unsigned char *in, unsigned char *out,
    uint32_t insize )
{
    unsigned char marker, symbol;
    uint32_t  i, inpos, outpos, length, offset;

    /* Do we have anything to uncompress? */
    if( insize < 1 )
    {
        return;
    }

    /* Get marker symbol from input stream */
    marker = in[ 0 ];
    inpos = 1;

    /* Main decompression loop */
    outpos = 0;
    do
    {
        symbol = in[ inpos ++ ];
        if( symbol == marker )
        {
            /* We had a marker byte */
            if( in[ inpos ] == 0 )
            {
                /* It was a single occurrence of the marker byte */
                out[ outpos ++ ] = marker;
                ++ inpos;
            }
            else
            {
                /* Extract true length and offset */
                inpos += _LZ_ReadVarSize( &length, &in[ inpos ] );
                inpos += _LZ_ReadVarSize( &offset, &in[ inpos ] );

                /* Copy corresponding data from history window */
                for( i = 0; i < length; ++ i )
                {
                    out[ outpos ] = out[ outpos - offset ];
                    ++ outpos;
                }
            }
        }
        else
        {
            /* No marker, plain copy */
            out[ outpos ++ ] = symbol;
        }
    }
    while( inpos < insize );
}

--- NEW FILE: lz.h ---
/*************************************************************************
* Name:        lz.h
* Author:      Marcus Geelnard
* Description: LZ77 coder/decoder interface.
* Reentrant:   Yes
*-------------------------------------------------------------------------
* Copyright (c) 2003-2006 Marcus Geelnard
*
* This software is provided 'as-is', without any express or implied
* warranty. In no event will the authors be held liable for any damages
* arising from the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
*
* 1. The origin of this software must not be misrepresented; you must not
*    claim that you wrote the original software. If you use this software
*    in a product, an acknowledgment in the product documentation would
*    be appreciated but is not required.
*
* 2. Altered source versions must be plainly marked as such, and must not
*    be misrepresented as being the original software.
*
* 3. This notice may not be removed or altered from any source
*    distribution.
*
* Marcus Geelnard
* marcus.geelnard at home.se
*************************************************************************/

#ifndef _lz_h_
#define _lz_h_

#ifdef __cplusplus
extern "C" {
#endif


/*************************************************************************
* Function prototypes
*************************************************************************/

int LZ_Compress( unsigned char *in, unsigned char *out,
                 uint32_t insize );
int LZ_CompressFast( unsigned char *in, unsigned char *out,
                     uint32_t insize, uint32_t *work );
void LZ_Uncompress( unsigned char *in, unsigned char *out,
                    uint32_t insize );


#ifdef __cplusplus
}
#endif

#endif /* _lz_h_ */

--- NEW FILE: volumes-at45db.xml ---
<volume_table>
  <volume name="SENSORLOG" size="262144" type="log" circular="true"/>
</volume_table>

--- NEW FILE: volumes-stm25p.xml ---
<volume_table>
  <volume name="SENSORLOG" size="1048576" type="log" circular="true"/>
</volume_table>



More information about the Tinyos-2-commits mailing list