diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright FPInsight (c) 2015
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * 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.
+
+    * Neither the name of Thierry Bourrillon nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT
+OWNER 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.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,26 @@
+# Welcome to heatshrink
+
+[![build status](https://travis-ci.org/fpinsight/heatshrink.svg?branch=master)](https://travis-ci.org/fpinsight/heatshrink)
+
+heatshrink is a haskell FFI for Scott Vokes's [heatshrink](https://github.com/atomicobject/heatshrink)
+, a data compression/decompression library for embedded/real-time systems.
+
+All files under cbits are copyright by "Scott Vokes <vokes.s@gmail.com>"
+
+haskell FFI curently only support decompression.
+
+# Join in!
+
+We are happy to receive bug reports, fixes, documentation enhancements,
+and other improvements.
+
+Please report bugs via the
+[github issue tracker](https://github.com/fpinsight/heatshrink/issues).
+
+Master [git repository](https://github.com/fpinsight/heatshrink):
+
+* `git clone git@github.com:fpinsight/heatshrink.git`
+
+# Authors
+
+This library is written and maintained by Thierry Bourrillon, <thierry.bourrillon@fpinsight.com>.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/cbits/heatshrink_common.h b/cbits/heatshrink_common.h
new file mode 100644
--- /dev/null
+++ b/cbits/heatshrink_common.h
@@ -0,0 +1,20 @@
+#ifndef HEATSHRINK_H
+#define HEATSHRINK_H
+
+#define HEATSHRINK_AUTHOR "Scott Vokes <vokes.s@gmail.com>"
+#define HEATSHRINK_URL "https://github.com/atomicobject/heatshrink"
+
+/* Version 0.4.1 */
+#define HEATSHRINK_VERSION_MAJOR 0
+#define HEATSHRINK_VERSION_MINOR 4
+#define HEATSHRINK_VERSION_PATCH 1
+
+#define HEATSHRINK_MIN_WINDOW_BITS 4
+#define HEATSHRINK_MAX_WINDOW_BITS 15
+
+#define HEATSHRINK_MIN_LOOKAHEAD_BITS 3
+
+#define HEATSHRINK_LITERAL_MARKER 0x01
+#define HEATSHRINK_BACKREF_MARKER 0x00
+
+#endif
diff --git a/cbits/heatshrink_config.h b/cbits/heatshrink_config.h
new file mode 100644
--- /dev/null
+++ b/cbits/heatshrink_config.h
@@ -0,0 +1,26 @@
+#ifndef HEATSHRINK_CONFIG_H
+#define HEATSHRINK_CONFIG_H
+
+/* Should functionality assuming dynamic allocation be used? */
+#ifndef HEATSHRINK_DYNAMIC_ALLOC
+#define HEATSHRINK_DYNAMIC_ALLOC 1
+#endif
+
+#if HEATSHRINK_DYNAMIC_ALLOC
+    /* Optional replacement of malloc/free */
+    #define HEATSHRINK_MALLOC(SZ) malloc(SZ)
+    #define HEATSHRINK_FREE(P, SZ) free(P)
+#else
+    /* Required parameters for static configuration */
+    #define HEATSHRINK_STATIC_INPUT_BUFFER_SIZE 32
+    #define HEATSHRINK_STATIC_WINDOW_BITS 8
+    #define HEATSHRINK_STATIC_LOOKAHEAD_BITS 4
+#endif
+
+/* Turn on logging for debugging. */
+#define HEATSHRINK_DEBUGGING_LOGS 0
+
+/* Use indexing for faster compression. (This requires additional space.) */
+#define HEATSHRINK_USE_INDEX 1
+
+#endif
diff --git a/cbits/heatshrink_decoder.c b/cbits/heatshrink_decoder.c
new file mode 100644
--- /dev/null
+++ b/cbits/heatshrink_decoder.c
@@ -0,0 +1,385 @@
+#include <stdlib.h>
+#include <string.h>
+#include "heatshrink_decoder.h"
+
+/* States for the polling state machine. */
+typedef enum {
+    HSDS_TAG_BIT,               /* tag bit */
+    HSDS_YIELD_LITERAL,         /* ready to yield literal byte */
+    HSDS_BACKREF_INDEX_MSB,     /* most significant byte of index */
+    HSDS_BACKREF_INDEX_LSB,     /* least significant byte of index */
+    HSDS_BACKREF_COUNT_MSB,     /* most significant byte of count */
+    HSDS_BACKREF_COUNT_LSB,     /* least significant byte of count */
+    HSDS_YIELD_BACKREF,         /* ready to yield back-reference */
+} HSD_state;
+
+#if HEATSHRINK_DEBUGGING_LOGS
+#include <stdio.h>
+#include <ctype.h>
+#include <assert.h>
+#define LOG(...) fprintf(stderr, __VA_ARGS__)
+#define ASSERT(X) assert(X)
+static const char *state_names[] = {
+    "tag_bit",
+    "yield_literal",
+    "backref_index_msb",
+    "backref_index_lsb",
+    "backref_count_msb",
+    "backref_count_lsb",
+    "yield_backref",
+};
+#else
+#define LOG(...) /* no-op */
+#define ASSERT(X) /* no-op */
+#endif
+
+typedef struct {
+    uint8_t *buf;               /* output buffer */
+    size_t buf_size;            /* buffer size */
+    size_t *output_size;        /* bytes pushed to buffer, so far */
+} output_info;
+
+#define NO_BITS ((uint16_t)-1)
+
+/* Forward references. */
+static uint16_t get_bits(heatshrink_decoder *hsd, uint8_t count);
+static void push_byte(heatshrink_decoder *hsd, output_info *oi, uint8_t byte);
+
+#if HEATSHRINK_DYNAMIC_ALLOC
+heatshrink_decoder *heatshrink_decoder_alloc(uint16_t input_buffer_size,
+                                             uint8_t window_sz2,
+                                             uint8_t lookahead_sz2) {
+    LOG("-- heatshrink_decoder_alloc %u %u %u\n", input_buffer_size, window_sz2, lookahead_sz2);
+    if ((window_sz2 < HEATSHRINK_MIN_WINDOW_BITS) ||
+        (window_sz2 > HEATSHRINK_MAX_WINDOW_BITS) ||
+        (input_buffer_size == 0) ||
+        (lookahead_sz2 < HEATSHRINK_MIN_LOOKAHEAD_BITS) ||
+        (lookahead_sz2 >= window_sz2)) {
+        return NULL;
+    }
+    size_t buffers_sz = (1 << window_sz2) + input_buffer_size;
+    uint8_t *buffers = HEATSHRINK_MALLOC(buffers_sz);
+    if (buffers == NULL) { return NULL; }
+    size_t sz = sizeof(heatshrink_decoder);
+    heatshrink_decoder *hsd = HEATSHRINK_MALLOC(sz);
+    if (hsd == NULL) {
+        LOG("-- failed to allocate buffers\n");
+        HEATSHRINK_FREE(buffers, buffers_sz);
+        return NULL;
+    }
+    hsd->buffers = buffers;
+    hsd->input_buffer_size = input_buffer_size;
+    hsd->window_sz2 = window_sz2;
+    hsd->lookahead_sz2 = lookahead_sz2;
+    heatshrink_decoder_reset(hsd);
+    LOG("-- allocated decoder with buffer size of %zu (%zu + %u + %u)\n",
+        sz, sizeof(heatshrink_decoder), (1 << window_sz2), input_buffer_size);
+    LOG("-- decoder : %p, buffers: %p\n", hsd, hsd->buffers);
+    return hsd;
+}
+
+void heatshrink_decoder_free(heatshrink_decoder *hsd) {
+    LOG("-- heatshrink_decoder_free %p\n", hsd);
+    if (hsd != NULL) {
+        size_t buffers_sz = (1 << hsd->window_sz2) + hsd->input_buffer_size;
+        size_t sz = sizeof(heatshrink_decoder);
+        HEATSHRINK_FREE(hsd->buffers, buffer_sz);
+        HEATSHRINK_FREE(hsd, sz);
+        (void)buffers_sz;   /* may not be used by free */
+        (void)sz;
+    }
+}
+#endif
+
+void heatshrink_decoder_reset(heatshrink_decoder *hsd) {
+    LOG("-- heatshrink_decoder_reset %p\n", hsd);
+    size_t buf_sz = 1 << HEATSHRINK_DECODER_WINDOW_BITS(hsd);
+    size_t input_sz = HEATSHRINK_DECODER_INPUT_BUFFER_SIZE(hsd);
+    memset(hsd->buffers, 0, buf_sz + input_sz);
+    hsd->state = HSDS_TAG_BIT;
+    hsd->input_size = 0;
+    hsd->input_index = 0;
+    hsd->bit_index = 0x00;
+    hsd->current_byte = 0x00;
+    hsd->output_count = 0;
+    hsd->output_index = 0;
+    hsd->head_index = 0;
+}
+
+/* Copy SIZE bytes into the decoder's input buffer, if it will fit. */
+HSD_sink_res heatshrink_decoder_sink(heatshrink_decoder *hsd,
+        uint8_t *in_buf, size_t size, size_t *input_size) {
+    LOG("-- heatshrink_decoder_sink %p %p %lu %p\n", hsd, in_buf, size, input_size);
+    if ((hsd == NULL) || (in_buf == NULL) || (input_size == NULL)) {
+        return HSDR_SINK_ERROR_NULL;
+    }
+
+    size_t rem = HEATSHRINK_DECODER_INPUT_BUFFER_SIZE(hsd) - hsd->input_size;
+    if (rem == 0) {
+        *input_size = 0;
+        return HSDR_SINK_FULL;
+    }
+
+    size = rem < size ? rem : size;
+    LOG("-- sinking %zd bytes\n", size);
+    /* copy into input buffer (at head of buffers) */
+    memcpy(&hsd->buffers[hsd->input_size], in_buf, size);
+    hsd->input_size += size;
+    *input_size = size;
+    return HSDR_SINK_OK;
+}
+
+
+/*****************
+ * Decompression *
+ *****************/
+
+#define BACKREF_COUNT_BITS(HSD) (HEATSHRINK_DECODER_LOOKAHEAD_BITS(HSD))
+#define BACKREF_INDEX_BITS(HSD) (HEATSHRINK_DECODER_WINDOW_BITS(HSD))
+
+// States
+static HSD_state st_tag_bit(heatshrink_decoder *hsd);
+static HSD_state st_yield_literal(heatshrink_decoder *hsd,
+    output_info *oi);
+static HSD_state st_backref_index_msb(heatshrink_decoder *hsd);
+static HSD_state st_backref_index_lsb(heatshrink_decoder *hsd);
+static HSD_state st_backref_count_msb(heatshrink_decoder *hsd);
+static HSD_state st_backref_count_lsb(heatshrink_decoder *hsd);
+static HSD_state st_yield_backref(heatshrink_decoder *hsd,
+    output_info *oi);
+
+HSD_poll_res heatshrink_decoder_poll(heatshrink_decoder *hsd,
+        uint8_t *out_buf, size_t out_buf_size, size_t *output_size) {
+
+    LOG("-- heatshrink_decoder_poll %p %p %lu %p\n", hsd, out_buf, out_buf_size, output_size);
+    if ((hsd == NULL) || (out_buf == NULL) || (output_size == NULL)) {
+        return HSDR_POLL_ERROR_NULL;
+    }
+    *output_size = 0;
+
+    output_info oi;
+    oi.buf = out_buf;
+    oi.buf_size = out_buf_size;
+    oi.output_size = output_size;
+
+    while (1) {
+        LOG("-- poll, state is %d (%s), input_size %d\n",
+            hsd->state, state_names[hsd->state], hsd->input_size);
+        uint8_t in_state = hsd->state;
+        switch (in_state) {
+        case HSDS_TAG_BIT:
+            hsd->state = st_tag_bit(hsd);
+            break;
+        case HSDS_YIELD_LITERAL:
+            hsd->state = st_yield_literal(hsd, &oi);
+            break;
+        case HSDS_BACKREF_INDEX_MSB:
+            hsd->state = st_backref_index_msb(hsd);
+            break;
+        case HSDS_BACKREF_INDEX_LSB:
+            hsd->state = st_backref_index_lsb(hsd);
+            break;
+        case HSDS_BACKREF_COUNT_MSB:
+            hsd->state = st_backref_count_msb(hsd);
+            break;
+        case HSDS_BACKREF_COUNT_LSB:
+            hsd->state = st_backref_count_lsb(hsd);
+            break;
+        case HSDS_YIELD_BACKREF:
+            hsd->state = st_yield_backref(hsd, &oi);
+            break;
+        default:
+            return HSDR_POLL_ERROR_UNKNOWN;
+        }
+
+        /* If the current state cannot advance, check if input or output
+         * buffer are exhausted. */
+        if (hsd->state == in_state) {
+            if (*output_size == out_buf_size) { return HSDR_POLL_MORE; }
+            return HSDR_POLL_EMPTY;
+        }
+    }
+}
+
+static HSD_state st_tag_bit(heatshrink_decoder *hsd) {
+    uint32_t bits = get_bits(hsd, 1);  // get tag bit
+    if (bits == NO_BITS) {
+        return HSDS_TAG_BIT;
+    } else if (bits) {
+        return HSDS_YIELD_LITERAL;
+    } else if (HEATSHRINK_DECODER_WINDOW_BITS(hsd) > 8) {
+        return HSDS_BACKREF_INDEX_MSB;
+    } else {
+        hsd->output_index = 0;
+        return HSDS_BACKREF_INDEX_LSB;
+    }
+}
+
+static HSD_state st_yield_literal(heatshrink_decoder *hsd,
+        output_info *oi) {
+    /* Emit a repeated section from the window buffer, and add it (again)
+     * to the window buffer. (Note that the repetition can include
+     * itself.)*/
+    if (*oi->output_size < oi->buf_size) {
+        uint16_t byte = get_bits(hsd, 8);
+        if (byte == NO_BITS) { return HSDS_YIELD_LITERAL; } /* out of input */
+        uint8_t *buf = &hsd->buffers[HEATSHRINK_DECODER_INPUT_BUFFER_SIZE(hsd)];
+        uint16_t mask = (1 << HEATSHRINK_DECODER_WINDOW_BITS(hsd))  - 1;
+        uint8_t c = byte & 0xFF;
+        LOG("-- emitting literal byte 0x%02x ('%c')\n", c, isprint(c) ? c : '.');
+        buf[hsd->head_index++ & mask] = c;
+        push_byte(hsd, oi, c);
+        return HSDS_TAG_BIT;
+    } else {
+        return HSDS_YIELD_LITERAL;
+    }
+}
+
+static HSD_state st_backref_index_msb(heatshrink_decoder *hsd) {
+    uint8_t bit_ct = BACKREF_INDEX_BITS(hsd);
+    ASSERT(bit_ct > 8);
+    uint16_t bits = get_bits(hsd, bit_ct - 8);
+    LOG("-- backref index (msb), got 0x%04x (+1)\n", bits);
+    if (bits == NO_BITS) { return HSDS_BACKREF_INDEX_MSB; }
+    hsd->output_index = bits << 8;
+    return HSDS_BACKREF_INDEX_LSB;
+}
+
+static HSD_state st_backref_index_lsb(heatshrink_decoder *hsd) {
+    uint8_t bit_ct = BACKREF_INDEX_BITS(hsd);
+    uint16_t bits = get_bits(hsd, bit_ct < 8 ? bit_ct : 8);
+    LOG("-- backref index (lsb), got 0x%04x (+1)\n", bits);
+    if (bits == NO_BITS) { return HSDS_BACKREF_INDEX_LSB; }
+    hsd->output_index |= bits;
+    hsd->output_index++;
+    uint8_t br_bit_ct = BACKREF_COUNT_BITS(hsd);
+    hsd->output_count = 0;
+    return (br_bit_ct > 8) ? HSDS_BACKREF_COUNT_MSB : HSDS_BACKREF_COUNT_LSB;
+}
+
+static HSD_state st_backref_count_msb(heatshrink_decoder *hsd) {
+    uint8_t br_bit_ct = BACKREF_COUNT_BITS(hsd);
+    ASSERT(br_bit_ct > 8);
+    uint16_t bits = get_bits(hsd, br_bit_ct - 8);
+    LOG("-- backref count (msb), got 0x%04x (+1)\n", bits);
+    if (bits == NO_BITS) { return HSDS_BACKREF_COUNT_MSB; }
+    hsd->output_count = bits << 8;
+    return HSDS_BACKREF_COUNT_LSB;
+}
+
+static HSD_state st_backref_count_lsb(heatshrink_decoder *hsd) {
+    uint8_t br_bit_ct = BACKREF_COUNT_BITS(hsd);
+    uint16_t bits = get_bits(hsd, br_bit_ct < 8 ? br_bit_ct : 8);
+    LOG("-- backref count (lsb), got 0x%04x (+1)\n", bits);
+    if (bits == NO_BITS) { return HSDS_BACKREF_COUNT_LSB; }
+    hsd->output_count |= bits;
+    hsd->output_count++;
+    return HSDS_YIELD_BACKREF;
+}
+
+static HSD_state st_yield_backref(heatshrink_decoder *hsd,
+        output_info *oi) {
+    size_t count = oi->buf_size - *oi->output_size;
+    if (count > 0) {
+        size_t i = 0;
+        if (hsd->output_count < count) count = hsd->output_count;
+        uint8_t *buf = &hsd->buffers[HEATSHRINK_DECODER_INPUT_BUFFER_SIZE(hsd)];
+        uint16_t mask = (1 << HEATSHRINK_DECODER_WINDOW_BITS(hsd)) - 1;
+        uint16_t neg_offset = hsd->output_index;
+        LOG("-- emitting %zu bytes from -%u bytes back\n", count, neg_offset);
+        ASSERT(neg_offset <= mask + 1);
+        ASSERT(count <= (size_t)(1 << BACKREF_COUNT_BITS(hsd)));
+
+        for (i=0; i<count; i++) {
+            uint8_t c = buf[(hsd->head_index - neg_offset) & mask];
+            push_byte(hsd, oi, c);
+            buf[hsd->head_index & mask] = c;
+            hsd->head_index++;
+            LOG("  -- ++ 0x%02x\n", c);
+        }
+        hsd->output_count -= count;
+        if (hsd->output_count == 0) { return HSDS_TAG_BIT; }
+    }
+    return HSDS_YIELD_BACKREF;
+}
+
+/* Get the next COUNT bits from the input buffer, saving incremental progress.
+ * Returns NO_BITS on end of input, or if more than 15 bits are requested. */
+static uint16_t get_bits(heatshrink_decoder *hsd, uint8_t count) {
+    uint16_t accumulator = 0;
+    int i = 0;
+    if (count > 15) { return NO_BITS; }
+    LOG("-- popping %u bit(s)\n", count);
+
+    /* If we aren't able to get COUNT bits, suspend immediately, because we
+     * don't track how many bits of COUNT we've accumulated before suspend. */
+    if (hsd->input_size == 0) {
+        if (hsd->bit_index < (1 << (count - 1))) { return NO_BITS; }
+    }
+
+    for (i = 0; i < count; i++) {
+        if (hsd->bit_index == 0x00) {
+            if (hsd->input_size == 0) {
+                LOG("  -- out of bits, suspending w/ accumulator of %u (0x%02x)\n",
+                    accumulator, accumulator);
+                return NO_BITS;
+            }
+            hsd->current_byte = hsd->buffers[hsd->input_index++];
+            LOG("  -- pulled byte 0x%02x\n", hsd->current_byte);
+            if (hsd->input_index == hsd->input_size) {
+                hsd->input_index = 0; /* input is exhausted */
+                hsd->input_size = 0;
+            }
+            hsd->bit_index = 0x80;
+        }
+        accumulator <<= 1;
+        if (hsd->current_byte & hsd->bit_index) {
+            accumulator |= 0x01;
+            if (0) {
+                LOG("  -- got 1, accumulator 0x%04x, bit_index 0x%02x\n",
+                accumulator, hsd->bit_index);
+            }
+        } else {
+            if (0) {
+                LOG("  -- got 0, accumulator 0x%04x, bit_index 0x%02x\n",
+                accumulator, hsd->bit_index);
+            }
+        }
+        hsd->bit_index >>= 1;
+    }
+
+    if (count > 1) { LOG("  -- accumulated %08x\n", accumulator); }
+    return accumulator;
+}
+
+HSD_finish_res heatshrink_decoder_finish(heatshrink_decoder *hsd) {
+    if (hsd == NULL) { return HSDR_FINISH_ERROR_NULL; }
+    switch (hsd->state) {
+    case HSDS_TAG_BIT:
+        return hsd->input_size == 0 ? HSDR_FINISH_DONE : HSDR_FINISH_MORE;
+
+    /* If we want to finish with no input, but are in these states, it's
+     * because the 0-bit padding to the last byte looks like a backref
+     * marker bit followed by all 0s for index and count bits. */
+    case HSDS_BACKREF_INDEX_LSB:
+    case HSDS_BACKREF_INDEX_MSB:
+    case HSDS_BACKREF_COUNT_LSB:
+    case HSDS_BACKREF_COUNT_MSB:
+        return hsd->input_size == 0 ? HSDR_FINISH_DONE : HSDR_FINISH_MORE;
+
+    /* If the output stream is padded with 0xFFs (possibly due to being in
+     * flash memory), also explicitly check the input size rather than
+     * uselessly returning MORE but yielding 0 bytes when polling. */
+    case HSDS_YIELD_LITERAL:
+        return hsd->input_size == 0 ? HSDR_FINISH_DONE : HSDR_FINISH_MORE;
+
+    default:
+        return HSDR_FINISH_MORE;
+    }
+}
+
+static void push_byte(heatshrink_decoder *hsd, output_info *oi, uint8_t byte) {
+    LOG(" -- pushing byte: 0x%02x ('%c')\n", byte, isprint(byte) ? byte : '.');
+    oi->buf[(*oi->output_size)++] = byte;
+    (void)hsd;
+}
diff --git a/cbits/heatshrink_decoder.h b/cbits/heatshrink_decoder.h
new file mode 100644
--- /dev/null
+++ b/cbits/heatshrink_decoder.h
@@ -0,0 +1,100 @@
+#ifndef HEATSHRINK_DECODER_H
+#define HEATSHRINK_DECODER_H
+
+#include <stdint.h>
+#include <stddef.h>
+#include "heatshrink_common.h"
+#include "heatshrink_config.h"
+
+typedef enum {
+    HSDR_SINK_OK,               /* data sunk, ready to poll */
+    HSDR_SINK_FULL,             /* out of space in internal buffer */
+    HSDR_SINK_ERROR_NULL=-1,    /* NULL argument */
+} HSD_sink_res;
+
+typedef enum {
+    HSDR_POLL_EMPTY,            /* input exhausted */
+    HSDR_POLL_MORE,             /* more data remaining, call again w/ fresh output buffer */
+    HSDR_POLL_ERROR_NULL=-1,    /* NULL arguments */
+    HSDR_POLL_ERROR_UNKNOWN=-2,
+} HSD_poll_res;
+
+typedef enum {
+    HSDR_FINISH_DONE,           /* output is done */
+    HSDR_FINISH_MORE,           /* more output remains */
+    HSDR_FINISH_ERROR_NULL=-1,  /* NULL arguments */
+} HSD_finish_res;
+
+#if HEATSHRINK_DYNAMIC_ALLOC
+#define HEATSHRINK_DECODER_INPUT_BUFFER_SIZE(BUF) \
+    ((BUF)->input_buffer_size)
+#define HEATSHRINK_DECODER_WINDOW_BITS(BUF) \
+    ((BUF)->window_sz2)
+#define HEATSHRINK_DECODER_LOOKAHEAD_BITS(BUF) \
+    ((BUF)->lookahead_sz2)
+#else
+#define HEATSHRINK_DECODER_INPUT_BUFFER_SIZE(_) \
+    HEATSHRINK_STATIC_INPUT_BUFFER_SIZE
+#define HEATSHRINK_DECODER_WINDOW_BITS(_) \
+    (HEATSHRINK_STATIC_WINDOW_BITS)
+#define HEATSHRINK_DECODER_LOOKAHEAD_BITS(BUF) \
+    (HEATSHRINK_STATIC_LOOKAHEAD_BITS)
+#endif
+
+typedef struct {
+    uint16_t input_size;        /* bytes in input buffer */
+    uint16_t input_index;       /* offset to next unprocessed input byte */
+    uint16_t output_count;      /* how many bytes to output */
+    uint16_t output_index;      /* index for bytes to output */
+    uint16_t head_index;        /* head of window buffer */
+    uint8_t state;              /* current state machine node */
+    uint8_t current_byte;       /* current byte of input */
+    uint8_t bit_index;          /* current bit index */
+
+#if HEATSHRINK_DYNAMIC_ALLOC
+    /* Fields that are only used if dynamically allocated. */
+    uint8_t window_sz2;         /* window buffer bits */
+    uint8_t lookahead_sz2;      /* lookahead bits */
+    uint16_t input_buffer_size; /* input buffer size */
+
+    /* Input buffer, then expansion window buffer */
+    uint8_t *buffers;
+#else
+    /* Input buffer, then expansion window buffer */
+    uint8_t buffers[(1 << HEATSHRINK_DECODER_WINDOW_BITS(_))
+        + HEATSHRINK_DECODER_INPUT_BUFFER_SIZE(_)];
+#endif
+} heatshrink_decoder;
+
+#if HEATSHRINK_DYNAMIC_ALLOC
+/* Allocate a decoder with an input buffer of INPUT_BUFFER_SIZE bytes,
+ * an expansion buffer size of 2^WINDOW_SZ2, and a lookahead
+ * size of 2^lookahead_sz2. (The window buffer and lookahead sizes
+ * must match the settings used when the data was compressed.)
+ * Returns NULL on error. */
+heatshrink_decoder *heatshrink_decoder_alloc(uint16_t input_buffer_size,
+    uint8_t expansion_buffer_sz2, uint8_t lookahead_sz2);
+
+/* Free a decoder. */
+void heatshrink_decoder_free(heatshrink_decoder *hsd);
+#endif
+
+/* Reset a decoder. */
+void heatshrink_decoder_reset(heatshrink_decoder *hsd);
+
+/* Sink at most SIZE bytes from IN_BUF into the decoder. *INPUT_SIZE is set to
+ * indicate how many bytes were actually sunk (in case a buffer was filled). */
+HSD_sink_res heatshrink_decoder_sink(heatshrink_decoder *hsd,
+    uint8_t *in_buf, size_t size, size_t *input_size);
+
+/* Poll for output from the decoder, copying at most OUT_BUF_SIZE bytes into
+ * OUT_BUF (setting *OUTPUT_SIZE to the actual amount copied). */
+HSD_poll_res heatshrink_decoder_poll(heatshrink_decoder *hsd,
+    uint8_t *out_buf, size_t out_buf_size, size_t *output_size);
+
+/* Notify the dencoder that the input stream is finished.
+ * If the return value is HSDR_FINISH_MORE, there is still more output, so
+ * call heatshrink_decoder_poll and repeat. */
+HSD_finish_res heatshrink_decoder_finish(heatshrink_decoder *hsd);
+
+#endif
diff --git a/cbits/heatshrink_encoder.c b/cbits/heatshrink_encoder.c
new file mode 100644
--- /dev/null
+++ b/cbits/heatshrink_encoder.c
@@ -0,0 +1,611 @@
+#include <stdlib.h>
+#include <string.h>
+#include <stdbool.h>
+#include "heatshrink_encoder.h"
+
+typedef enum {
+    HSES_NOT_FULL,              /* input buffer not full enough */
+    HSES_FILLED,                /* buffer is full */
+    HSES_SEARCH,                /* searching for patterns */
+    HSES_YIELD_TAG_BIT,         /* yield tag bit */
+    HSES_YIELD_LITERAL,         /* emit literal byte */
+    HSES_YIELD_BR_INDEX,        /* yielding backref index */
+    HSES_YIELD_BR_LENGTH,       /* yielding backref length */
+    HSES_SAVE_BACKLOG,          /* copying buffer to backlog */
+    HSES_FLUSH_BITS,            /* flush bit buffer */
+    HSES_DONE,                  /* done */
+} HSE_state;
+
+#if HEATSHRINK_DEBUGGING_LOGS
+#include <stdio.h>
+#include <ctype.h>
+#include <assert.h>
+#define LOG(...) fprintf(stderr, __VA_ARGS__)
+#define ASSERT(X) assert(X)
+static const char *state_names[] = {
+    "not_full",
+    "filled",
+    "search",
+    "yield_tag_bit",
+    "yield_literal",
+    "yield_br_index",
+    "yield_br_length",
+    "save_backlog",
+    "flush_bits",
+    "done",
+};
+#else
+#define LOG(...) /* no-op */
+#define ASSERT(X) /* no-op */
+#endif
+
+// Encoder flags
+enum {
+    FLAG_IS_FINISHING = 0x01,
+};
+
+typedef struct {
+    uint8_t *buf;               /* output buffer */
+    size_t buf_size;            /* buffer size */
+    size_t *output_size;        /* bytes pushed to buffer, so far */
+} output_info;
+
+#define MATCH_NOT_FOUND ((uint16_t)-1)
+
+static uint16_t get_input_offset(heatshrink_encoder *hse);
+static uint16_t get_input_buffer_size(heatshrink_encoder *hse);
+static uint16_t get_lookahead_size(heatshrink_encoder *hse);
+static void add_tag_bit(heatshrink_encoder *hse, output_info *oi, uint8_t tag);
+static int can_take_byte(output_info *oi);
+static int is_finishing(heatshrink_encoder *hse);
+static void save_backlog(heatshrink_encoder *hse);
+
+/* Push COUNT (max 8) bits to the output buffer, which has room. */
+static void push_bits(heatshrink_encoder *hse, uint8_t count, uint8_t bits,
+    output_info *oi);
+static uint8_t push_outgoing_bits(heatshrink_encoder *hse, output_info *oi);
+static void push_literal_byte(heatshrink_encoder *hse, output_info *oi);
+
+#if HEATSHRINK_DYNAMIC_ALLOC
+heatshrink_encoder *heatshrink_encoder_alloc(uint8_t window_sz2,
+        uint8_t lookahead_sz2) {
+    if ((window_sz2 < HEATSHRINK_MIN_WINDOW_BITS) ||
+        (window_sz2 > HEATSHRINK_MAX_WINDOW_BITS) ||
+        (lookahead_sz2 < HEATSHRINK_MIN_LOOKAHEAD_BITS) ||
+        (lookahead_sz2 >= window_sz2)) {
+        return NULL;
+    }
+
+    /* Note: 2 * the window size is used because the buffer needs to fit
+     * (1 << window_sz2) bytes for the current input, and an additional
+     * (1 << window_sz2) bytes for the previous buffer of input, which
+     * will be scanned for useful backreferences. */
+    size_t buf_sz = (2 << window_sz2);
+
+    heatshrink_encoder *hse = HEATSHRINK_MALLOC(sizeof(*hse) + buf_sz);
+    if (hse == NULL) { return NULL; }
+    uint8_t *buffer = HEATSHRINK_MALLOC(buf_sz);
+    if (buffer == NULL) {
+        HEATSHRINK_FREE(hse, sizeof(*hse));
+        return NULL;
+    }
+    hse->buffer = buffer;
+    hse->window_sz2 = window_sz2;
+    hse->lookahead_sz2 = lookahead_sz2;
+    heatshrink_encoder_reset(hse);
+
+#if HEATSHRINK_USE_INDEX
+    size_t index_sz = buf_sz*sizeof(uint16_t);
+    hse->search_index = HEATSHRINK_MALLOC(index_sz + sizeof(struct hs_index));
+    if (hse->search_index == NULL) {
+        HEATSHRINK_FREE(hse, sizeof(*hse) + buf_sz);
+        return NULL;
+    }
+    hse->search_index->size = index_sz;
+#endif
+
+    LOG("-- allocated encoder with buffer size of %zu (%u byte input size)\n",
+        buf_sz, get_input_buffer_size(hse));
+    return hse;
+}
+
+void heatshrink_encoder_free(heatshrink_encoder *hse) {
+    size_t buf_sz = (2 << HEATSHRINK_ENCODER_WINDOW_BITS(hse));
+    HEATSHRINK_FREE(hse->buffer, buf_sz);
+#if HEATSHRINK_USE_INDEX
+    size_t index_sz = sizeof(struct hs_index) + hse->search_index->size;
+    HEATSHRINK_FREE(hse->search_index, index_sz);
+    (void)index_sz;
+#endif
+    HEATSHRINK_FREE(hse, sizeof(heatshrink_encoder));
+    (void)buf_sz;
+}
+#endif
+
+void heatshrink_encoder_reset(heatshrink_encoder *hse) {
+    size_t buf_sz = (2 << HEATSHRINK_ENCODER_WINDOW_BITS(hse));
+    memset(hse->buffer, 0, buf_sz);
+    hse->input_size = 0;
+    hse->state = HSES_NOT_FULL;
+    hse->match_scan_index = 0;
+    hse->flags = 0;
+    hse->bit_index = 0x80;
+    hse->current_byte = 0x00;
+    hse->match_length = 0;
+
+    hse->outgoing_bits = 0x0000;
+    hse->outgoing_bits_count = 0;
+
+    #ifdef LOOP_DETECT
+    hse->loop_detect = (uint32_t)-1;
+    #endif
+}
+
+HSE_sink_res heatshrink_encoder_sink(heatshrink_encoder *hse,
+        uint8_t *in_buf, size_t size, size_t *input_size) {
+    if ((hse == NULL) || (in_buf == NULL) || (input_size == NULL)) {
+        return HSER_SINK_ERROR_NULL;
+    }
+
+    /* Sinking more content after saying the content is done, tsk tsk */
+    if (is_finishing(hse)) { return HSER_SINK_ERROR_MISUSE; }
+
+    /* Sinking more content before processing is done */
+    if (hse->state != HSES_NOT_FULL) { return HSER_SINK_ERROR_MISUSE; }
+
+    uint16_t write_offset = get_input_offset(hse) + hse->input_size;
+    uint16_t ibs = get_input_buffer_size(hse);
+    uint16_t rem = ibs - hse->input_size;
+    uint16_t cp_sz = rem < size ? rem : size;
+
+    memcpy(&hse->buffer[write_offset], in_buf, cp_sz);
+    *input_size = cp_sz;
+    hse->input_size += cp_sz;
+
+    LOG("-- sunk %u bytes (of %zu) into encoder at %d, input buffer now has %u\n",
+        cp_sz, size, write_offset, hse->input_size);
+    if (cp_sz == rem) {
+        LOG("-- internal buffer is now full\n");
+        hse->state = HSES_FILLED;
+    }
+
+    return HSER_SINK_OK;
+}
+
+
+/***************
+ * Compression *
+ ***************/
+
+static uint16_t find_longest_match(heatshrink_encoder *hse, uint16_t start,
+    uint16_t end, const uint16_t maxlen, uint16_t *match_length);
+static void do_indexing(heatshrink_encoder *hse);
+
+static HSE_state st_step_search(heatshrink_encoder *hse);
+static HSE_state st_yield_tag_bit(heatshrink_encoder *hse,
+    output_info *oi);
+static HSE_state st_yield_literal(heatshrink_encoder *hse,
+    output_info *oi);
+static HSE_state st_yield_br_index(heatshrink_encoder *hse,
+    output_info *oi);
+static HSE_state st_yield_br_length(heatshrink_encoder *hse,
+    output_info *oi);
+static HSE_state st_save_backlog(heatshrink_encoder *hse);
+static HSE_state st_flush_bit_buffer(heatshrink_encoder *hse,
+    output_info *oi);
+
+HSE_poll_res heatshrink_encoder_poll(heatshrink_encoder *hse,
+        uint8_t *out_buf, size_t out_buf_size, size_t *output_size) {
+    if ((hse == NULL) || (out_buf == NULL) || (output_size == NULL)) {
+        return HSER_POLL_ERROR_NULL;
+    }
+    if (out_buf_size == 0) {
+        LOG("-- MISUSE: output buffer size is 0\n");
+        return HSER_POLL_ERROR_MISUSE;
+    }
+    *output_size = 0;
+
+    output_info oi;
+    oi.buf = out_buf;
+    oi.buf_size = out_buf_size;
+    oi.output_size = output_size;
+
+    while (1) {
+        LOG("-- polling, state %u (%s), flags 0x%02x\n",
+            hse->state, state_names[hse->state], hse->flags);
+
+        uint8_t in_state = hse->state;
+        switch (in_state) {
+        case HSES_NOT_FULL:
+            return HSER_POLL_EMPTY;
+        case HSES_FILLED:
+            do_indexing(hse);
+            hse->state = HSES_SEARCH;
+            break;
+        case HSES_SEARCH:
+            hse->state = st_step_search(hse);
+            break;
+        case HSES_YIELD_TAG_BIT:
+            hse->state = st_yield_tag_bit(hse, &oi);
+            break;
+        case HSES_YIELD_LITERAL:
+            hse->state = st_yield_literal(hse, &oi);
+            break;
+        case HSES_YIELD_BR_INDEX:
+            hse->state = st_yield_br_index(hse, &oi);
+            break;
+        case HSES_YIELD_BR_LENGTH:
+            hse->state = st_yield_br_length(hse, &oi);
+            break;
+        case HSES_SAVE_BACKLOG:
+            hse->state = st_save_backlog(hse);
+            break;
+        case HSES_FLUSH_BITS:
+            hse->state = st_flush_bit_buffer(hse, &oi);
+        case HSES_DONE:
+            return HSER_POLL_EMPTY;
+        default:
+            LOG("-- bad state %s\n", state_names[hse->state]);
+            return HSER_POLL_ERROR_MISUSE;
+        }
+
+        if (hse->state == in_state) {
+            /* Check if output buffer is exhausted. */
+            if (*output_size == out_buf_size) return HSER_POLL_MORE;
+        }
+    }
+}
+
+HSE_finish_res heatshrink_encoder_finish(heatshrink_encoder *hse) {
+    if (hse == NULL) { return HSER_FINISH_ERROR_NULL; }
+    LOG("-- setting is_finishing flag\n");
+    hse->flags |= FLAG_IS_FINISHING;
+    if (hse->state == HSES_NOT_FULL) { hse->state = HSES_FILLED; }
+    return hse->state == HSES_DONE ? HSER_FINISH_DONE : HSER_FINISH_MORE;
+}
+
+static HSE_state st_step_search(heatshrink_encoder *hse) {
+    uint16_t window_length = get_input_buffer_size(hse);
+    uint16_t lookahead_sz = get_lookahead_size(hse);
+    uint16_t msi = hse->match_scan_index;
+    LOG("## step_search, scan @ +%d (%d/%d), input size %d\n",
+        msi, hse->input_size + msi, 2*window_length, hse->input_size);
+
+    bool fin = is_finishing(hse);
+    if (msi > hse->input_size - (fin ? 1 : lookahead_sz)) {
+        /* Current search buffer is exhausted, copy it into the
+         * backlog and await more input. */
+        LOG("-- end of search @ %d\n", msi);
+        return fin ? HSES_FLUSH_BITS : HSES_SAVE_BACKLOG;
+    }
+
+    uint16_t input_offset = get_input_offset(hse);
+    uint16_t end = input_offset + msi;
+    uint16_t start = end - window_length;
+
+    uint16_t max_possible = lookahead_sz;
+    if (hse->input_size - msi < lookahead_sz) {
+        max_possible = hse->input_size - msi;
+    }
+
+    uint16_t match_length = 0;
+    uint16_t match_pos = find_longest_match(hse,
+        start, end, max_possible, &match_length);
+
+    if (match_pos == MATCH_NOT_FOUND) {
+        LOG("ss Match not found\n");
+        hse->match_scan_index++;
+        hse->match_length = 0;
+        return HSES_YIELD_TAG_BIT;
+    } else {
+        LOG("ss Found match of %d bytes at %d\n", match_length, match_pos);
+        hse->match_pos = match_pos;
+        hse->match_length = match_length;
+        ASSERT(match_pos <= 1 << HEATSHRINK_ENCODER_WINDOW_BITS(hse) /*window_length*/);
+
+        return HSES_YIELD_TAG_BIT;
+    }
+}
+
+static HSE_state st_yield_tag_bit(heatshrink_encoder *hse,
+        output_info *oi) {
+    if (can_take_byte(oi)) {
+        if (hse->match_length == 0) {
+            add_tag_bit(hse, oi, HEATSHRINK_LITERAL_MARKER);
+            return HSES_YIELD_LITERAL;
+        } else {
+            add_tag_bit(hse, oi, HEATSHRINK_BACKREF_MARKER);
+            hse->outgoing_bits = hse->match_pos - 1;
+            hse->outgoing_bits_count = HEATSHRINK_ENCODER_WINDOW_BITS(hse);
+            return HSES_YIELD_BR_INDEX;
+        }
+    } else {
+        return HSES_YIELD_TAG_BIT; /* output is full, continue */
+    }
+}
+
+static HSE_state st_yield_literal(heatshrink_encoder *hse,
+        output_info *oi) {
+    if (can_take_byte(oi)) {
+        push_literal_byte(hse, oi);
+        return HSES_SEARCH;
+    } else {
+        return HSES_YIELD_LITERAL;
+    }
+}
+
+static HSE_state st_yield_br_index(heatshrink_encoder *hse,
+        output_info *oi) {
+    if (can_take_byte(oi)) {
+        LOG("-- yielding backref index %u\n", hse->match_pos);
+        if (push_outgoing_bits(hse, oi) > 0) {
+            return HSES_YIELD_BR_INDEX; /* continue */
+        } else {
+            hse->outgoing_bits = hse->match_length - 1;
+            hse->outgoing_bits_count = HEATSHRINK_ENCODER_LOOKAHEAD_BITS(hse);
+            return HSES_YIELD_BR_LENGTH; /* done */
+        }
+    } else {
+        return HSES_YIELD_BR_INDEX; /* continue */
+    }
+}
+
+static HSE_state st_yield_br_length(heatshrink_encoder *hse,
+        output_info *oi) {
+    if (can_take_byte(oi)) {
+        LOG("-- yielding backref length %u\n", hse->match_length);
+        if (push_outgoing_bits(hse, oi) > 0) {
+            return HSES_YIELD_BR_LENGTH;
+        } else {
+            hse->match_scan_index += hse->match_length;
+            hse->match_length = 0;
+            return HSES_SEARCH;
+        }
+    } else {
+        return HSES_YIELD_BR_LENGTH;
+    }
+}
+
+static HSE_state st_save_backlog(heatshrink_encoder *hse) {
+    LOG("-- saving backlog\n");
+    save_backlog(hse);
+    return HSES_NOT_FULL;
+}
+
+static HSE_state st_flush_bit_buffer(heatshrink_encoder *hse,
+        output_info *oi) {
+    if (hse->bit_index == 0x80) {
+        LOG("-- done!\n");
+        return HSES_DONE;
+    } else if (can_take_byte(oi)) {
+        LOG("-- flushing remaining byte (bit_index == 0x%02x)\n", hse->bit_index);
+        oi->buf[(*oi->output_size)++] = hse->current_byte;
+        LOG("-- done!\n");
+        return HSES_DONE;
+    } else {
+        return HSES_FLUSH_BITS;
+    }
+}
+
+static void add_tag_bit(heatshrink_encoder *hse, output_info *oi, uint8_t tag) {
+    LOG("-- adding tag bit: %d\n", tag);
+    push_bits(hse, 1, tag, oi);
+}
+
+static uint16_t get_input_offset(heatshrink_encoder *hse) {
+    return get_input_buffer_size(hse);
+}
+
+static uint16_t get_input_buffer_size(heatshrink_encoder *hse) {
+    return (1 << HEATSHRINK_ENCODER_WINDOW_BITS(hse));
+    (void)hse;
+}
+
+static uint16_t get_lookahead_size(heatshrink_encoder *hse) {
+    return (1 << HEATSHRINK_ENCODER_LOOKAHEAD_BITS(hse));
+    (void)hse;
+}
+
+static void do_indexing(heatshrink_encoder *hse) {
+#if HEATSHRINK_USE_INDEX
+    /* Build an index array I that contains flattened linked lists
+     * for the previous instances of every byte in the buffer.
+     *
+     * For example, if buf[200] == 'x', then index[200] will either
+     * be an offset i such that buf[i] == 'x', or a negative offset
+     * to indicate end-of-list. This significantly speeds up matching,
+     * while only using sizeof(uint16_t)*sizeof(buffer) bytes of RAM.
+     *
+     * Future optimization options:
+     * 1. Since any negative value represents end-of-list, the other
+     *    15 bits could be used to improve the index dynamically.
+     *
+     * 2. Likewise, the last lookahead_sz bytes of the index will
+     *    not be usable, so temporary data could be stored there to
+     *    dynamically improve the index.
+     * */
+    struct hs_index *hsi = HEATSHRINK_ENCODER_INDEX(hse);
+    int16_t last[256];
+    memset(last, 0xFF, sizeof(last));
+
+    uint8_t * const data = hse->buffer;
+    int16_t * const index = hsi->index;
+
+    const uint16_t input_offset = get_input_offset(hse);
+    const uint16_t end = input_offset + hse->input_size;
+
+    for (uint16_t i=0; i<end; i++) {
+        uint8_t v = data[i];
+        int16_t lv = last[v];
+        index[i] = lv;
+        last[v] = i;
+    }
+#else
+    (void)hse;
+#endif
+}
+
+static int is_finishing(heatshrink_encoder *hse) {
+    return hse->flags & FLAG_IS_FINISHING;
+}
+
+static int can_take_byte(output_info *oi) {
+    return *oi->output_size < oi->buf_size;
+}
+
+/* Return the longest match for the bytes at buf[end:end+maxlen] between
+ * buf[start] and buf[end-1]. If no match is found, return -1. */
+static uint16_t find_longest_match(heatshrink_encoder *hse, uint16_t start,
+        uint16_t end, const uint16_t maxlen, uint16_t *match_length) {
+    LOG("-- scanning for match of buf[%u:%u] between buf[%u:%u] (max %u bytes)\n",
+        end, end + maxlen, start, end + maxlen - 1, maxlen);
+    uint8_t *buf = hse->buffer;
+
+    uint16_t match_maxlen = 0;
+    uint16_t match_index = MATCH_NOT_FOUND;
+
+    uint16_t len = 0;
+    uint8_t * const needlepoint = &buf[end];
+#if HEATSHRINK_USE_INDEX
+    struct hs_index *hsi = HEATSHRINK_ENCODER_INDEX(hse);
+    int16_t pos = hsi->index[end];
+
+    while (pos - (int16_t)start >= 0) {
+        uint8_t * const pospoint = &buf[pos];
+        len = 0;
+
+        /* Only check matches that will potentially beat the current maxlen.
+         * This is redundant with the index if match_maxlen is 0, but the
+         * added branch overhead to check if it == 0 seems to be worse. */
+        if (pospoint[match_maxlen] != needlepoint[match_maxlen]) {
+            pos = hsi->index[pos];
+            continue;
+        }
+
+        for (len = 1; len < maxlen; len++) {
+            if (pospoint[len] != needlepoint[len]) break;
+        }
+
+        if (len > match_maxlen) {
+            match_maxlen = len;
+            match_index = pos;
+            if (len == maxlen) { break; } /* won't find better */
+        }
+        pos = hsi->index[pos];
+    }
+#else
+    for (int16_t pos=end - 1; pos - (int16_t)start >= 0; pos--) {
+        uint8_t * const pospoint = &buf[pos];
+        if ((pospoint[match_maxlen] == needlepoint[match_maxlen])
+            && (*pospoint == *needlepoint)) {
+            for (len=1; len<maxlen; len++) {
+                if (0) {
+                    LOG("  --> cmp buf[%d] == 0x%02x against %02x (start %u)\n",
+                        pos + len, pospoint[len], needlepoint[len], start);
+                }
+                if (pospoint[len] != needlepoint[len]) { break; }
+            }
+            if (len > match_maxlen) {
+                match_maxlen = len;
+                match_index = pos;
+                if (len == maxlen) { break; } /* don't keep searching */
+            }
+        }
+    }
+#endif
+
+    const size_t break_even_point =
+      (1 + HEATSHRINK_ENCODER_WINDOW_BITS(hse) +
+          HEATSHRINK_ENCODER_LOOKAHEAD_BITS(hse));
+
+    /* Instead of comparing break_even_point against 8*match_maxlen,
+     * compare match_maxlen against break_even_point/8 to avoid
+     * overflow. Since MIN_WINDOW_BITS and MIN_LOOKAHEAD_BITS are 4 and
+     * 3, respectively, break_even_point/8 will always be at least 1. */
+    if (match_maxlen > (break_even_point / 8)) {
+        LOG("-- best match: %u bytes at -%u\n",
+            match_maxlen, end - match_index);
+        *match_length = match_maxlen;
+        return end - match_index;
+    }
+    LOG("-- none found\n");
+    return MATCH_NOT_FOUND;
+}
+
+static uint8_t push_outgoing_bits(heatshrink_encoder *hse, output_info *oi) {
+    uint8_t count = 0;
+    uint8_t bits = 0;
+    if (hse->outgoing_bits_count > 8) {
+        count = 8;
+        bits = hse->outgoing_bits >> (hse->outgoing_bits_count - 8);
+    } else {
+        count = hse->outgoing_bits_count;
+        bits = hse->outgoing_bits;
+    }
+
+    if (count > 0) {
+        LOG("-- pushing %d outgoing bits: 0x%02x\n", count, bits);
+        push_bits(hse, count, bits, oi);
+        hse->outgoing_bits_count -= count;
+    }
+    return count;
+}
+
+/* Push COUNT (max 8) bits to the output buffer, which has room.
+ * Bytes are set from the lowest bits, up. */
+static void push_bits(heatshrink_encoder *hse, uint8_t count, uint8_t bits,
+        output_info *oi) {
+    ASSERT(count <= 8);
+    LOG("++ push_bits: %d bits, input of 0x%02x\n", count, bits);
+
+    /* If adding a whole byte and at the start of a new output byte,
+     * just push it through whole and skip the bit IO loop. */
+    if (count == 8 && hse->bit_index == 0x80) {
+        oi->buf[(*oi->output_size)++] = bits;
+    } else {
+        for (int i=count - 1; i>=0; i--) {
+            bool bit = bits & (1 << i);
+            if (bit) { hse->current_byte |= hse->bit_index; }
+            if (0) {
+                LOG("  -- setting bit %d at bit index 0x%02x, byte => 0x%02x\n",
+                    bit ? 1 : 0, hse->bit_index, hse->current_byte);
+            }
+            hse->bit_index >>= 1;
+            if (hse->bit_index == 0x00) {
+                hse->bit_index = 0x80;
+                LOG(" > pushing byte 0x%02x\n", hse->current_byte);
+                oi->buf[(*oi->output_size)++] = hse->current_byte;
+                hse->current_byte = 0x00;
+            }
+        }
+    }
+}
+
+static void push_literal_byte(heatshrink_encoder *hse, output_info *oi) {
+    uint16_t processed_offset = hse->match_scan_index - 1;
+    uint16_t input_offset = get_input_offset(hse) + processed_offset;
+    uint8_t c = hse->buffer[input_offset];
+    LOG("-- yielded literal byte 0x%02x ('%c') from +%d\n",
+        c, isprint(c) ? c : '.', input_offset);
+    push_bits(hse, 8, c, oi);
+}
+
+static void save_backlog(heatshrink_encoder *hse) {
+    size_t input_buf_sz = get_input_buffer_size(hse);
+
+    uint16_t msi = hse->match_scan_index;
+
+    /* Copy processed data to beginning of buffer, so it can be
+     * used for future matches. Don't bother checking whether the
+     * input is less than the maximum size, because if it isn't,
+     * we're done anyway. */
+    uint16_t rem = input_buf_sz - msi; // unprocessed bytes
+    uint16_t shift_sz = input_buf_sz + rem;
+
+    memmove(&hse->buffer[0],
+        &hse->buffer[input_buf_sz - rem],
+        shift_sz);
+
+    hse->match_scan_index = 0;
+    hse->input_size -= input_buf_sz - rem;
+}
diff --git a/cbits/heatshrink_encoder.h b/cbits/heatshrink_encoder.h
new file mode 100644
--- /dev/null
+++ b/cbits/heatshrink_encoder.h
@@ -0,0 +1,109 @@
+#ifndef HEATSHRINK_ENCODER_H
+#define HEATSHRINK_ENCODER_H
+
+#include <stdint.h>
+#include <stddef.h>
+#include "heatshrink_common.h"
+#include "heatshrink_config.h"
+
+typedef enum {
+    HSER_SINK_OK,               /* data sunk into input buffer */
+    HSER_SINK_ERROR_NULL=-1,    /* NULL argument */
+    HSER_SINK_ERROR_MISUSE=-2,  /* API misuse */
+} HSE_sink_res;
+
+typedef enum {
+    HSER_POLL_EMPTY,            /* input exhausted */
+    HSER_POLL_MORE,             /* poll again for more output  */
+    HSER_POLL_ERROR_NULL=-1,    /* NULL argument */
+    HSER_POLL_ERROR_MISUSE=-2,  /* API misuse */
+} HSE_poll_res;
+
+typedef enum {
+    HSER_FINISH_DONE,           /* encoding is complete */
+    HSER_FINISH_MORE,           /* more output remaining; use poll */
+    HSER_FINISH_ERROR_NULL=-1,  /* NULL argument */
+} HSE_finish_res;
+
+#if HEATSHRINK_DYNAMIC_ALLOC
+#define HEATSHRINK_ENCODER_WINDOW_BITS(HSE) \
+    ((HSE)->window_sz2)
+#define HEATSHRINK_ENCODER_LOOKAHEAD_BITS(HSE) \
+    ((HSE)->lookahead_sz2)
+#define HEATSHRINK_ENCODER_INDEX(HSE) \
+    ((HSE)->search_index)
+struct hs_index {
+    uint16_t size;
+    int16_t index[];
+};
+#else
+#define HEATSHRINK_ENCODER_WINDOW_BITS(_) \
+    (HEATSHRINK_STATIC_WINDOW_BITS)
+#define HEATSHRINK_ENCODER_LOOKAHEAD_BITS(_) \
+    (HEATSHRINK_STATIC_LOOKAHEAD_BITS)
+#define HEATSHRINK_ENCODER_INDEX(HSE) \
+    (&(HSE)->search_index)
+struct hs_index {
+    uint16_t size;
+    int16_t index[2 << HEATSHRINK_STATIC_WINDOW_BITS];
+};
+#endif
+
+typedef struct {
+    uint16_t input_size;        /* bytes in input buffer */
+    uint16_t match_scan_index;
+    uint16_t match_length;
+    uint16_t match_pos;
+    uint16_t outgoing_bits;     /* enqueued outgoing bits */
+    uint8_t outgoing_bits_count;
+    uint8_t flags;
+    uint8_t state;              /* current state machine node */
+    uint8_t current_byte;       /* current byte of output */
+    uint8_t bit_index;          /* current bit index */
+#if HEATSHRINK_DYNAMIC_ALLOC
+    uint8_t window_sz2;         /* 2^n size of window */
+    uint8_t lookahead_sz2;      /* 2^n size of lookahead */
+#if HEATSHRINK_USE_INDEX
+    struct hs_index *search_index;
+#endif
+    /* input buffer and / sliding window for expansion */
+    uint8_t *buffer;
+#else
+    #if HEATSHRINK_USE_INDEX
+        struct hs_index search_index;
+    #endif
+    /* input buffer and / sliding window for expansion */
+    uint8_t buffer[2 << HEATSHRINK_ENCODER_WINDOW_BITS(_)];
+#endif
+} heatshrink_encoder;
+
+#if HEATSHRINK_DYNAMIC_ALLOC
+/* Allocate a new encoder struct and its buffers.
+ * Returns NULL on error. */
+heatshrink_encoder *heatshrink_encoder_alloc(uint8_t window_sz2,
+    uint8_t lookahead_sz2);
+
+/* Free an encoder. */
+void heatshrink_encoder_free(heatshrink_encoder *hse);
+#endif
+
+/* Reset an encoder. */
+void heatshrink_encoder_reset(heatshrink_encoder *hse);
+
+/* Sink up to SIZE bytes from IN_BUF into the encoder.
+ * INPUT_SIZE is set to the number of bytes actually sunk (in case a
+ * buffer was filled.). */
+HSE_sink_res heatshrink_encoder_sink(heatshrink_encoder *hse,
+    uint8_t *in_buf, size_t size, size_t *input_size);
+
+/* Poll for output from the encoder, copying at most OUT_BUF_SIZE bytes into
+ * OUT_BUF (setting *OUTPUT_SIZE to the actual amount copied). */
+HSE_poll_res heatshrink_encoder_poll(heatshrink_encoder *hse,
+    uint8_t *out_buf, size_t out_buf_size, size_t *output_size);
+
+/* Notify the encoder that the input stream is finished.
+ * If the return value is HSER_FINISH_MORE, there is still more output, so
+ * call heatshrink_encoder_poll and repeat. */
+HSE_finish_res heatshrink_encoder_finish(heatshrink_encoder *hse);
+
+#endif
diff --git a/files/frame1_w12l8.golden b/files/frame1_w12l8.golden
new file mode 100644
--- /dev/null
+++ b/files/frame1_w12l8.golden
@@ -0,0 +1,1 @@
+{"from":{"deviceId":"Spoony1","unit":"DataChunk"},"t":"2015-10-16T15:10:25.824Z","count":29,"elements":[{"n":"TEMP","count":1,"records":[{"i":0,"t":"2015-10-16T15:10:19.999Z","q":"good","v":24.99732}]},{"n":"FREQ","count":1,"records":[{"i":0,"t":"2015-10-16T15:10:19.999Z","q":"good","v":50.00000}]},{"n":"VRMSA","count":1,"records":[{"i":0,"t":"2015-10-16T15:10:19.999Z","q":"good","v":219.72705}]},{"n":"VRMSB","count":1,"records":[{"i":0,"t":"2015-10-16T15:10:19.999Z","q":"good","v":220.09834}]},{"n":"VRMSC","count":1,"records":[{"i":0,"t":"2015-10-16T15:10:19.999Z","q":"good","v":220.04170}]},{"n":"IRMSA","count":1,"records":[{"i":0,"t":"2015-10-16T15:10:19.999Z","q":"good","v":9.10000}]},{"n":"IRMSB","count":1,"records":[{"i":0,"t":"2015-10-16T15:10:19.999Z","q":"good","v":9.93344}]},{"n":"IRMSC","count":1,"records":[{"i":0,"t":"2015-10-16T15:10:19.999Z","q":"good","v":9.56120}]},{"n":"WATTHRA","count":1,"records":[{"i":0,"t":"2015-10-16T15:10:19.999Z","q":"good","v":0.27771}]},{"n":"WATTHRB","count":1,"records":[{"i":0,"t":"2015-10-16T15:10:19.999Z","q":"good","v":0.30365}]},{"n":"WATTHRC","count":1,"records":[{"i":0,"t":"2015-10-16T15:10:19.999Z","q":"good","v":0.29220}]},{"n":"VARHRA","count":1,"records":[{"i":0,"t":"2015-10-16T15:10:19.999Z","q":"good","v":0.00000}]},{"n":"VARHRB","count":1,"records":[{"i":0,"t":"2015-10-16T15:10:19.999Z","q":"good","v":0.00000}]},{"n":"VARHRC","count":1,"records":[{"i":0,"t":"2015-10-16T15:10:19.999Z","q":"good","v":0.00000}]},{"n":"VAHRA","count":1,"records":[{"i":0,"t":"2015-10-16T15:10:19.999Z","q":"good","v":0.27771}]},{"n":"VAHRB","count":1,"records":[{"i":0,"t":"2015-10-16T15:10:19.999Z","q":"good","v":0.30365}]},{"n":"VAHRC","count":1,"records":[{"i":0,"t":"2015-10-16T15:10:19.999Z","q":"good","v":0.29220}]},{"n":"WATTA","count":1,"records":[{"i":0,"t":"2015-10-16T15:10:19.999Z","q":"good","v":1999.51623}]},{"n":"WATTB","count":1,"records":[{"i":0,"t":"2015-10-16T15:10:19.999Z","q":"good","v":2186.33593}]},{"n":"WATTC","count":1,"records":[{"i":0,"t":"2015-10-16T15:10:19.999Z","q":"good","v":2103.86279}]},{"n":"VARA","count":1,"records":[{"i":0,"t":"2015-10-16T15:10:19.999Z","q":"good","v":0.00000}]},{"n":"VARB","count":1,"records":[{"i":0,"t":"2015-10-16T15:10:19.999Z","q":"good","v":0.00000}]},{"n":"VARC","count":1,"records":[{"i":0,"t":"2015-10-16T15:10:19.999Z","q":"good","v":0.00000}]},{"n":"VAA","count":1,"records":[{"i":0,"t":"2015-10-16T15:10:19.999Z","q":"good","v":1999.51623}]},{"n":"VAB","count":1,"records":[{"i":0,"t":"2015-10-16T15:10:19.999Z","q":"good","v":2186.33593}]},{"n":"VAC","count":1,"records":[{"i":0,"t":"2015-10-16T15:10:19.999Z","q":"good","v":2103.86279}]},{"n":"PFA","count":1,"records":[{"i":0,"t":"2015-10-16T15:10:19.999Z","q":"good","v":1.00000}]},{"n":"PFB","count":1,"records":[{"i":0,"t":"2015-10-16T15:10:19.999Z","q":"good","v":1.00000}]},{"n":"PFC","count":1,"records":[{"i":0,"t":"2015-10-16T15:10:19.999Z","q":"good","v":1.00000}]}]}
diff --git a/files/frame1_w12l8.hex b/files/frame1_w12l8.hex
new file mode 100644
Binary files /dev/null and b/files/frame1_w12l8.hex differ
diff --git a/files/frame2_w12l8.golden b/files/frame2_w12l8.golden
new file mode 100644
--- /dev/null
+++ b/files/frame2_w12l8.golden
@@ -0,0 +1,1 @@
+{"from":{"deviceId":"Spoony1","unit":"ODMDataChunk"},"t":"2016-01-29T15:01:32.028Z","count":29,"elements":[{"n":"TEMP","count":1,"records":[{"i":0,"t":"2016-01-29T15:01:31.998Z","q":"good","v":24.96719}]},{"n":"FREQ","count":1,"records":[{"i":0,"t":"2016-01-29T15:01:31.998Z","q":"good","v":50.00000}]},{"n":"VRMSA","count":1,"records":[{"i":0,"t":"2016-01-29T15:01:31.998Z","q":"good","v":220.15684}]},{"n":"VRMSB","count":1,"records":[{"i":0,"t":"2016-01-29T15:01:31.998Z","q":"good","v":219.30027}]},{"n":"VRMSC","count":1,"records":[{"i":0,"t":"2016-01-29T15:01:31.998Z","q":"good","v":219.67214}]},{"n":"IRMSA","count":1,"records":[{"i":0,"t":"2016-01-29T15:01:31.998Z","q":"good","v":-5.37061}]},{"n":"IRMSB","count":1,"records":[{"i":0,"t":"2016-01-29T15:01:31.998Z","q":"good","v":7.28793}]},{"n":"IRMSC","count":1,"records":[{"i":0,"t":"2016-01-29T15:01:31.998Z","q":"good","v":4.85941}]},{"n":"WATTHRA","count":1,"records":[{"i":0,"t":"2016-01-29T15:01:31.998Z","q":"good","v":0.23364}]},{"n":"WATTHRB","count":1,"records":[{"i":0,"t":"2016-01-29T15:01:31.998Z","q":"good","v":7.42876}]},{"n":"WATTHRC","count":1,"records":[{"i":0,"t":"2016-01-29T15:01:31.998Z","q":"good","v":5.86496}]},{"n":"VARHRA","count":1,"records":[{"i":0,"t":"2016-01-29T15:01:31.998Z","q":"good","v":0.01221}]},{"n":"VARHRB","count":1,"records":[{"i":0,"t":"2016-01-29T15:01:31.998Z","q":"good","v":0.14761}]},{"n":"VARHRC","count":1,"records":[{"i":0,"t":"2016-01-29T15:01:31.998Z","q":"good","v":0.14548}]},{"n":"VAHRA","count":1,"records":[{"i":0,"t":"2016-01-29T15:01:31.998Z","q":"good","v":0.23396}]},{"n":"VAHRB","count":1,"records":[{"i":0,"t":"2016-01-29T15:01:31.998Z","q":"good","v":7.43212}]},{"n":"VAHRC","count":1,"records":[{"i":0,"t":"2016-01-29T15:01:31.998Z","q":"good","v":5.86826}]},{"n":"WATTA","count":1,"records":[{"i":0,"t":"2016-01-29T15:01:31.998Z","q":"good","v":-1181.78686}]},{"n":"WATTB","count":1,"records":[{"i":0,"t":"2016-01-29T15:01:31.998Z","q":"good","v":1597.95654}]},{"n":"WATTC","count":1,"records":[{"i":0,"t":"2016-01-29T15:01:31.998Z","q":"good","v":1066.00219}]},{"n":"VARA","count":1,"records":[{"i":0,"t":"2016-01-29T15:01:31.998Z","q":"good","v":-37.34854}]},{"n":"VARB","count":1,"records":[{"i":0,"t":"2016-01-29T15:01:31.998Z","q":"good","v":30.45286}]},{"n":"VARC","count":1,"records":[{"i":0,"t":"2016-01-29T15:01:31.998Z","q":"good","v":56.12364}]},{"n":"VAA","count":1,"records":[{"i":0,"t":"2016-01-29T15:01:31.998Z","q":"good","v":-1182.37683}]},{"n":"VAB","count":1,"records":[{"i":0,"t":"2016-01-29T15:01:31.998Z","q":"good","v":1598.24670}]},{"n":"VAC","count":1,"records":[{"i":0,"t":"2016-01-29T15:01:31.998Z","q":"good","v":1067.47863}]},{"n":"PFA","count":1,"records":[{"i":0,"t":"2016-01-29T15:01:31.998Z","q":"good","v":0.99950}]},{"n":"PFB","count":1,"records":[{"i":0,"t":"2016-01-29T15:01:31.998Z","q":"good","v":0.99981}]},{"n":"PFC","count":1,"records":[{"i":0,"t":"2016-01-29T15:01:31.998Z","q":"good","v":0.99861}]}]}
diff --git a/files/frame2_w12l8.hex b/files/frame2_w12l8.hex
new file mode 100644
Binary files /dev/null and b/files/frame2_w12l8.hex differ
diff --git a/heatshrink.cabal b/heatshrink.cabal
new file mode 100644
--- /dev/null
+++ b/heatshrink.cabal
@@ -0,0 +1,75 @@
+-- This file has been generated from package.yaml by hpack version 0.17.0.
+--
+-- see: https://github.com/sol/hpack
+
+name:           heatshrink
+version:        0.1.0.0
+synopsis:       Compression and decompression using heatshrink
+description:    Please see README.md
+category:       FFI
+homepage:       https://github.com/fpinsight/heatshrink#readme
+bug-reports:    https://github.com/fpinsight/heatshrink/issues
+author:         Thierry Bourrillon
+maintainer:     thierry.bourrillon@fpinsight.com
+copyright:      2016 Thierry Bourrillon
+license:        BSD3
+license-file:   LICENSE
+build-type:     Simple
+cabal-version:  >= 1.10
+
+extra-source-files:
+    cbits/heatshrink_common.h
+    cbits/heatshrink_config.h
+    cbits/heatshrink_decoder.h
+    cbits/heatshrink_encoder.h
+    files/frame1_w12l8.golden
+    files/frame1_w12l8.hex
+    files/frame2_w12l8.golden
+    files/frame2_w12l8.hex
+    README.md
+
+source-repository head
+  type: git
+  location: https://github.com/fpinsight/heatshrink
+
+library
+  hs-source-dirs:
+      src
+  ghc-options: -Wall
+  cc-options: -std=c11
+  include-dirs:
+      cbits
+  c-sources:
+      cbits/heatshrink_decoder.c
+      cbits/heatshrink_encoder.c
+  build-depends:
+      bytestring
+    , cereal
+    , base >=4.7 && <5
+  build-tools:
+      c2hs
+  exposed-modules:
+      Codec.Compression.Heatshrink
+  other-modules:
+      Codec.Compression.Heatshrink.Internal
+  default-language: Haskell2010
+
+test-suite heatshrink-test
+  type: exitcode-stdio-1.0
+  main-is: Tests.hs
+  hs-source-dirs:
+      tests
+  ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      bytestring
+    , cereal
+    , base
+    , heatshrink
+    , pcre-heavy
+    , tasty
+    , tasty-golden
+    , tasty-hunit
+    , text
+  other-modules:
+      Codec.Compression.Heatshrink.Tests
+  default-language: Haskell2010
diff --git a/src/Codec/Compression/Heatshrink.hs b/src/Codec/Compression/Heatshrink.hs
new file mode 100644
--- /dev/null
+++ b/src/Codec/Compression/Heatshrink.hs
@@ -0,0 +1,132 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE BangPatterns #-}
+
+--------------------------------------------------------------------------------
+module Codec.Compression.Heatshrink
+    (
+      -- * decoding
+      Decoder
+    , DecodeParams(..)
+    , defaultDecodeParams
+    , decoderWithParams
+    , decode
+    , decodeWith
+
+      -- * encoding
+
+    ) where
+
+
+--------------------------------------------------------------------------------
+import           Codec.Compression.Heatshrink.Internal
+import qualified Data.ByteString          as B
+import           Data.ByteString.Internal hiding (ByteString)
+import qualified Data.ByteString.Lazy     as BL
+import           Foreign.ForeignPtr
+import           Foreign.Ptr
+
+
+--------------------------------------------------------------------------------
+data DecodeParams = DecodeParams {
+      inputBufferSize :: Int
+    , expansionBufferBits :: Int
+    , lookAheadBits :: Int
+    } deriving (Show, Eq)
+
+
+--------------------------------------------------------------------------------
+defaultDecodeParams :: DecodeParams
+defaultDecodeParams = DecodeParams {
+                        inputBufferSize = 1024
+                      , expansionBufferBits = 12
+                      , lookAheadBits = 8
+                      }
+
+
+--------------------------------------------------------------------------------
+data Decoder = Decoder !HeatshrinkDecoderTPtr !DecodeParams
+             deriving (Eq, Show)
+
+
+--------------------------------------------------------------------------------
+decoderWithParams :: DecodeParams -> Decoder
+decoderWithParams p = Decoder ptr p
+    where
+      ptr = heatshrinkDecoderAlloc
+            (inputBufferSize p)
+            (expansionBufferBits p)
+            (lookAheadBits p)
+
+
+--------------------------------------------------------------------------------
+decodeWith :: DecodeParams -> BL.ByteString -> IO BL.ByteString
+decodeWith p bs = decode decoder bs
+    where decoder = decoderWithParams p
+
+
+--------------------------------------------------------------------------------
+decode :: Decoder -> BL.ByteString -> IO BL.ByteString
+decode decoder bs = do
+  let Decoder d _ = decoder
+  heatshrinkDecoderReset d
+  decodedChunks <- mapM (decodeChunk d) (BL.toChunks bs)
+  let decoded = BL.concat decodedChunks
+  f <- heatshrinkDecoderFinish d
+  case f of
+    HsdrFinishDone -> return decoded
+    HsdrFinishMore -> do
+                      leftover <- pollAll d 4096
+                      return $ (BL.concat decodedChunks) `BL.append` leftover
+    e -> fail $ show e
+
+
+--------------------------------------------------------------------------------
+decodeChunk :: HeatshrinkDecoderTPtr -> B.ByteString -> IO BL.ByteString
+decodeChunk d bs = do
+  go d bs BL.empty
+    where
+      go :: HeatshrinkDecoderTPtr -> B.ByteString -> BL.ByteString -> IO BL.ByteString
+      go d bs acc = if bs == B.empty then
+                        return acc
+                    else do
+                      res <- sink d bs
+                      case res of
+                        (HsdrSinkOk, n) -> do
+                                 let rem = B.drop n bs
+                                 dec <- pollAll d 4096
+                                 go d rem (BL.append acc dec)
+                        (e, _) -> fail $ show e
+
+
+--------------------------------------------------------------------------------
+sink :: HeatshrinkDecoderTPtr -> B.ByteString -> IO (HsdSinkRes, Int)
+sink d bs = do
+  let (PS p o l) = bs
+  (st, sz) <- withForeignPtr p (\ptr -> heatshrinkDecoderSink d (plusPtr ptr o) (fromIntegral l))
+  return (st, fromIntegral sz)
+
+
+--------------------------------------------------------------------------------
+poll :: HeatshrinkDecoderTPtr -> Int -> IO (HsdPollRes, BL.ByteString)
+poll d sz = do
+  (bs, st) <- createAndTrim' sz (\p -> do
+                                 (s, sz') <- heatshrinkDecoderPoll d p (fromIntegral sz)
+                                 return (0, fromIntegral sz', s)
+                              )
+  return (st, BL.fromStrict bs)
+
+
+--------------------------------------------------------------------------------
+pollAll :: HeatshrinkDecoderTPtr -> Int -> IO (BL.ByteString)
+pollAll d sz = pollAll' BL.empty d sz
+    where
+      pollAll' :: BL.ByteString -> HeatshrinkDecoderTPtr -> Int -> IO BL.ByteString
+      pollAll' acc d sz = do
+        res <- poll d sz
+        case res of
+          (HsdrPollMore, !bs) -> pollAll' (BL.append acc bs) d sz
+          (HsdrPollEmpty, !bs) -> return $ BL.append acc bs
+          (_, _) -> fail "error in pollAll"
+
+
+--------------------------------------------------------------------------------
diff --git a/src/Codec/Compression/Heatshrink/Internal.chs b/src/Codec/Compression/Heatshrink/Internal.chs
new file mode 100644
--- /dev/null
+++ b/src/Codec/Compression/Heatshrink/Internal.chs
@@ -0,0 +1,137 @@
+{-# LANGUAGE EmptyDataDecls #-}
+{-# LANGUAGE ForeignFunctionInterface #-}
+
+
+--------------------------------------------------------------------------------
+module Codec.Compression.Heatshrink.Internal where
+
+import Foreign.C.Types
+import Foreign.Ptr
+import Foreign.Storable
+import Foreign.Marshal.Alloc
+import Data.Word
+
+
+--------------------------------------------------------------------------------
+{#pointer *size_t as CSizePtr -> CSize #}
+{#pointer *uint8_t as CUCharPtr -> CUChar #}
+
+
+-- * decoding
+--------------------------------------------------------------------------------
+#include "heatshrink_decoder.h"
+
+--------------------------------------------------------------------------------
+{#enum HSD_sink_res as HsdSinkRes {underscoreToCase} deriving (Show, Eq) #}
+{#enum HSD_poll_res as HsdPollRes {underscoreToCase} deriving (Show, Eq) #}
+{#enum HSD_finish_res as HsdFinishRes {underscoreToCase} deriving (Show, Eq) #}
+
+--------------------------------------------------------------------------------
+data HeatshrinkDecoderT
+
+--------------------------------------------------------------------------------
+{#pointer *heatshrink_decoder as HeatshrinkDecoderTPtr foreign finalizer heatshrink_decoder_free #}
+--{#pointer *heatshrink_decoder as HeatshrinkDecoderTPtr foreign #}
+
+--------------------------------------------------------------------------------
+
+
+--------------------------------------------------------------------------------
+{#fun pure unsafe heatshrink_decoder_alloc as ^
+  { `Int'
+  , `Int'
+  , `Int'} -> `HeatshrinkDecoderTPtr'
+#}
+
+
+--------------------------------------------------------------------------------
+{#fun unsafe heatshrink_decoder_free as ^
+  { `HeatshrinkDecoderTPtr' } -> `()'
+#}
+
+--------------------------------------------------------------------------------
+{#fun unsafe heatshrink_decoder_reset as ^
+  { `HeatshrinkDecoderTPtr' } -> `()'
+#}
+
+
+--------------------------------------------------------------------------------
+{#fun unsafe heatshrink_decoder_sink as ^
+  { `HeatshrinkDecoderTPtr'
+  , castPtr `Ptr Word8'
+  , cIntConv `CSize'
+  , alloca- `CSize' peek* } -> `HsdSinkRes'
+#}
+
+
+--------------------------------------------------------------------------------
+{#fun unsafe heatshrink_decoder_poll as ^
+  { `HeatshrinkDecoderTPtr'
+  , castPtr `Ptr Word8'
+  , cIntConv `CSize'
+  , alloca- `CSize' peek* } -> `HsdPollRes'
+#}
+
+
+--------------------------------------------------------------------------------
+{#fun unsafe heatshrink_decoder_finish as ^
+  { `HeatshrinkDecoderTPtr' } -> `HsdFinishRes'
+#}
+
+
+--------------------------------------------------------------------------------
+cIntConv :: (Integral a, Num b) =>  a -> b
+cIntConv = fromIntegral
+
+
+--------------------------------------------------------------------------------
+
+-- encoding
+#include "heatshrink_encoder.h"
+
+{#enum HSE_sink_res as HseSinkRes {underscoreToCase} deriving (Show, Eq) #}
+{#enum HSE_poll_res as HsePollRes {underscoreToCase} deriving (Show, Eq) #}
+{#enum HSE_finish_res as HseFinishRes {underscoreToCase} deriving (Show, Eq) #}
+
+data HeatshrinkEncoderT
+
+{#pointer *heatshrink_encoder as HeatshrinkEncoderTPtr foreign finalizer heatshrink_encoder_free #}
+
+{#fun pure unsafe heatshrink_encoder_alloc as ^
+  { `Int'
+  , `Int'} -> `HeatshrinkEncoderTPtr'
+#}
+
+--------------------------------------------------------------------------------
+{#fun unsafe heatshrink_encoder_free as ^
+  { `HeatshrinkEncoderTPtr' } -> `()'
+#}
+
+--------------------------------------------------------------------------------
+{#fun unsafe heatshrink_encoder_reset as ^
+  { `HeatshrinkEncoderTPtr' } -> `()'
+#}
+
+
+--------------------------------------------------------------------------------
+{#fun unsafe heatshrink_encoder_sink as ^
+  { `HeatshrinkEncoderTPtr'
+  , castPtr `Ptr Word8'
+  , cIntConv `CSize'
+  , alloca- `CSize' peek* } -> `HseSinkRes'
+#}
+
+
+--------------------------------------------------------------------------------
+{#fun unsafe heatshrink_encoder_poll as ^
+  { `HeatshrinkEncoderTPtr'
+  , castPtr `Ptr Word8'
+  , cIntConv `CSize'
+  , alloca- `CSize' peek* } -> `HsePollRes'
+#}
+
+
+--------------------------------------------------------------------------------
+{#fun unsafe heatshrink_encoder_finish as ^
+  { `HeatshrinkEncoderTPtr' } -> `HseFinishRes'
+#}
diff --git a/tests/Codec/Compression/Heatshrink/Tests.hs b/tests/Codec/Compression/Heatshrink/Tests.hs
new file mode 100644
--- /dev/null
+++ b/tests/Codec/Compression/Heatshrink/Tests.hs
@@ -0,0 +1,48 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+
+--------------------------------------------------------------------------------
+module Codec.Compression.Heatshrink.Tests (tests) where
+
+
+--------------------------------------------------------------------------------
+import           Codec.Compression.Heatshrink
+import qualified Data.ByteString.Lazy as BL
+import           System.IO.Unsafe
+import           Test.Tasty
+import           Test.Tasty.Golden
+import           Text.Regex.PCRE.Heavy
+
+
+--------------------------------------------------------------------------------
+tests :: TestTree
+tests = testGroup "Codec.Compression.Heatshrink"
+        [ testGroup "decoding files" $ unsafePerformIO goldenTests
+        ]
+
+
+--------------------------------------------------------------------------------
+decodeFile :: DecodeParams-> FilePath -> FilePath -> IO ()
+decodeFile p s t  = do
+  c <- BL.readFile s
+  u <- decodeWith p c
+  BL.writeFile t u
+
+
+--------------------------------------------------------------------------------
+goldenTests :: IO [TestTree]
+goldenTests = do
+  files <- findByExtension [".golden"] "files"
+  let test = map (scan [re|((.*)/(\w+)_w(\d+)l(\d+))\.golden|]) files
+  return $ map createTest test
+
+
+--------------------------------------------------------------------------------
+createTest :: [(String, [String])] -> TestTree
+createTest [(f, [noext, dir, base, sw, sl])] = goldenVsFile f f dstFile (decodeFile params srcFile dstFile)
+    where
+      params = DecodeParams 1024 w l
+      srcFile = noext ++ ".hex"
+      dstFile = noext ++ ".tmp"
+      w = read sw :: Int
+      l = read sl :: Int
diff --git a/tests/Tests.hs b/tests/Tests.hs
new file mode 100644
--- /dev/null
+++ b/tests/Tests.hs
@@ -0,0 +1,15 @@
+--------------------------------------------------------------------------------
+import Test.Tasty
+
+--------------------------------------------------------------------------------
+import qualified Codec.Compression.Heatshrink.Tests
+
+--------------------------------------------------------------------------------
+main :: IO ()
+main = defaultMain tests
+
+
+--------------------------------------------------------------------------------
+tests :: TestTree
+tests = testGroup "Tests"
+        [ Codec.Compression.Heatshrink.Tests.tests ]
