private void shareLog() {

    synchronized (this) {

      // if we're logging save the log file
      if (afrAlarmLogging) {
        try {
          final String filename = new SimpleDateFormat("yyyyMMdd_HHmmss'.csv'").format(new Date());

          File sdcard = Environment.getExternalStorageDirectory();
          File dir = new File(sdcard.getAbsolutePath() + "/AdaptiveTuner/");
          dir.mkdirs();

          File file = new File(dir, filename);
          FileOutputStream f = new FileOutputStream(file);

          // write our header
          f.write("timestamp, rpm, map, targetafr, afr, refafr, wat, mat\n".getBytes());

          ArrayList<LogItem> items = afrAlarmLogItems.getItems();
          Iterator<LogItem> iterator = items.iterator();

          while (iterator.hasNext()) {
            final LogItem item = (LogItem) iterator.next();
            f.write(item.getLogBytes());
          }

          afrAlarmLogItems.getItems().clear();

          f.flush();
          f.close();

          menuShareLog.setVisible(false);

          Toast.makeText(
                  getApplicationContext(),
                  String.format(
                      "Log saved as %s%s%s", sdcard.getAbsolutePath(), "/AdaptiveTuner/", filename),
                  Toast.LENGTH_LONG)
              .show();
          if (DEBUG_MODE)
            Log.d(
                TAG,
                String.format(
                    "Log saved as %s%s%s", sdcard.getAbsolutePath(), "/AdaptiveTuner/", filename));

          Intent share = new Intent(Intent.ACTION_SEND);
          share.setType("text/plain");
          share.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://" + file.getPath()));
          startActivity(Intent.createChooser(share, getText(R.string.share_log)));

        } catch (Exception e) {
          Toast.makeText(getApplicationContext(), e.getMessage(), Toast.LENGTH_LONG).show();
        }
      }
    }
  }
  private static void process4096Response(final String data) {

    final String[] buf = data.substring(data.indexOf(EIGHT_REGISTERS), data.length()).split(" ");

    if (ModbusRTU.validCRC(buf, REGISTER_4096_LENGTH)) {
      imgStatus.setBackgroundColor(Color.GREEN);
      dataArray.clear();

      final int rpm = Integer.parseInt(buf[3] + buf[4], 16);
      final int map = Integer.parseInt(buf[5] + buf[6], 16);
      final int mat = getTemperatureValue(buf[7] + buf[8]);
      final int wat = getTemperatureValue(buf[9] + buf[10]);

      final float afr = Integer.parseInt(buf[14], 16) / 10f;
      final float referenceAfr = Integer.parseInt(buf[13], 16) / 10f;

      tps = Integer.parseInt(buf[17] + buf[18], 16);

      iatNeedle.setValue(mat);
      waterNeedle.setValue(Integer.parseInt(buf[9] + buf[10], 16) * 9 / 5 + 32);
      mapNeedle.setValue(map);

      {
        float afrVal = afr * 100;
        float targetAfrVal = targetAFR * 100;

        if (afrVal > AFR_MAX) afrVal = AFR_MAX;
        if (afrVal < AFR_MIN) afrVal = AFR_MIN;

        if (targetAfrVal > AFR_MAX) targetAfrVal = AFR_MAX;
        if (targetAfrVal < AFR_MIN) targetAfrVal = AFR_MIN;

        afrNeedle.setValue(AFR_MAX - afrVal + AFR_MIN);
        targetAfrNeedle.setValue(AFR_MAX - targetAfrVal + AFR_MIN);
      }

      if (rpm >= 200) lastRPM = rpm;
      dataArray.add(String.format("RPM\n%d", lastRPM));
      rpmNeedle.setValue(lastRPM);

      dataArray.add(String.format("MAP\n%d kPa", map));
      dataArray.add(String.format("MAT\n%d\u00B0 %s", mat, getTemperatureSymbol()));
      dataArray.add(String.format("AFR\n%.1f (%.1f)", afr, referenceAfr));
      dataArray.add("TAFR\n" + (targetAFR != 0f ? String.format("%.1f", targetAFR) : "--.-"));
      dataArray.add(String.format("WAT\n%d\u00B0 %s", wat, getTemperatureSymbol()));

      // alarm stuff
      if (gridData.getChildAt(3) != null && gridData.getChildAt(5) != null) {
        // water temperature
        gridData.getChildAt(5).setBackgroundColor(Color.TRANSPARENT);
        if (waterTempPref) {
          if (wat < minimumWaterTemp) gridData.getChildAt(5).setBackgroundColor(Color.BLUE);
          if (wat > maximumWaterTemp) gridData.getChildAt(5).setBackgroundColor(Color.RED);
        }

        // afr vs target alarm
        gridData.getChildAt(3).setBackgroundColor(Color.TRANSPARENT);
        if (afrNotEqualTargetPref) {
          final float threshold = targetAFR * (afrNotEqualTargetTolerance * .01f);
          if (Math.abs(targetAFR - afr) >= threshold) {
            if (afr > targetAFR) {
              gridData.getChildAt(3).setBackgroundColor(Color.RED);
            } else {
              gridData.getChildAt(3).setBackgroundColor(Color.BLUE);
            }

            if (afrAlarmLogging) {
              LogItem newItem = afrAlarmLogItems.newLogItem();
              newItem.setAfr(afr);
              newItem.setReferenceAfr(referenceAfr);
              newItem.setMap(map);
              newItem.setMat(mat);
              newItem.setRpm(rpm);
              newItem.setTargetAfr(targetAFR);
              newItem.setWat(wat);

              afrAlarmLogItems.getItems().add(newItem);

              if (!menuShareLog.isVisible()) {
                menuShareLog.setVisible(true);
              }
            }
          }
        }
      }

      if (DEBUG_MODE) Log.d(TAG, "Processed " + lastRegister + " response: " + data);
      sendRequest(REGISTER_4140_PLUS_SIX);
      return;

    } else {
      if (DEBUG_MODE) Log.d(TAG, "bad CRC for " + lastRegister + ": " + data);
      imgStatus.setBackgroundColor(Color.RED);
      sendRequest();
    }
  }