/*
 * Copyright (c) 2020, Pycom Limited.
 *
 * This software is licensed under the GNU GPL version 3 or any
 * later version, with permitted additional terms. For more information
 * see the Pycom Licence v1.0 document supplied with this file, or
 * available at https://www.pycom.io/opensource/licensing
 */

#include "py/mpconfig.h"
#include "py/obj.h"
#include "py/objstr.h"
#include "py/runtime.h"
#include "mperror.h"
#include "updater.h"
#include "modled.h"

#include "esp_system.h"
#include "nvs_flash.h"
#include "nvs.h"

#include "pycom_config.h"
#include "mpexception.h"
#include "machpin.h"
#include "driver/rmt.h"
#include "driver/gpio.h"

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#include "antenna.h"
#include "py/mphal.h"

#include "bootmgr.h"
#include "updater.h"

#include "mptask.h"

#include "modmachine.h"
#include "esp32chipinfo.h"
#include "modwlan.h"

#include <string.h>

extern led_info_t led_info;

#define NVS_NAMESPACE                           "PY_NVM"

#define WDT_ON_BOOT_MIN_TIMEOUT_MS              (5000)

static nvs_handle pycom_nvs_handle;
boot_info_t boot_info;
uint32_t boot_info_offset;

static void modpycom_bootmgr(uint8_t boot_partition, uint8_t fs_type, uint8_t safeboot, bool reset);

void modpycom_init0(void) {
    if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &pycom_nvs_handle) != ESP_OK) {
        mp_printf(&mp_plat_print, "Error while opening Pycom NVS name space\n");
    }
    rmt_driver_install(RMT_CHANNEL_0, 1000, 0);
    if (updater_read_boot_info (&boot_info, &boot_info_offset) == false) {
        mp_printf(&mp_plat_print, "Error reading bootloader information!\n");
    }
}

static bool is_empty(uint8_t* value, uint8_t size) {
    bool ret_val = true;
    for (int i=0; i < size; i++) {
        if (value[i] != 0xFF) {
            ret_val = false;
        }
    }
    return ret_val;
}

static void modpycom_bootmgr(uint8_t boot_partition, uint8_t fs_type, uint8_t safeboot, bool reset) {
    bool update_part = false;
    bool update_fstype = false;
    bool update_safeboot = false;

    if (boot_partition < 255) {
        if ((boot_partition <= IMG_ACT_UPDATE1) && (boot_info.ActiveImg != boot_partition)) {
            boot_info.PrevImg = boot_info.ActiveImg;
            boot_info.ActiveImg = (uint32_t)boot_partition;
            boot_info.Status = IMG_STATUS_CHECK;
            update_part = true;
        } else {
            nlr_raise(mp_obj_new_exception_msg(&mp_type_ValueError, "Error invalid boot partition! or partition already active!"));
        }
    }
    if (safeboot < 255) {
        if ((safeboot <= 1) && (safeboot != boot_info.safeboot)) {
            if(safeboot)
            {
                boot_info.safeboot = (uint32_t)SAFE_BOOT_SW;
            }
            else
            {
                boot_info.safeboot = (uint32_t)0x00;
            }
            update_safeboot = true;
        } else {
            nlr_raise(mp_obj_new_exception_msg(&mp_type_ValueError, "Error safeboot must be True or False!"));
        }
    }
    if (fs_type < 255) {
        if (fs_type <= 1) {
            if(config_get_boot_fs_type() != fs_type)
            {
                config_set_boot_fs_type(fs_type);
                update_fstype = true;
            }
        }
        else {
            nlr_raise(mp_obj_new_exception_msg(&mp_type_ValueError, "Error invalid filesystem type!"));
        }
    }
    if (update_part || update_safeboot) {
        if (updater_write_boot_info (&boot_info, boot_info_offset) == false) {
            nlr_raise(mp_obj_new_exception_msg(&mp_type_ValueError, "Error writing bootloader information!"));
        }
        if(update_part)
        {
            machine_reset();
        }
    }
    if((update_fstype || update_safeboot) && reset)
    {
        machine_reset();
    }
}

/******************************************************************************/
// Micro Python bindings

STATIC mp_obj_t mod_pycom_heartbeat (mp_uint_t n_args, const mp_obj_t *args) {
#ifndef RGB_LED_DISABLE
    if (n_args) {
        mperror_enable_heartbeat (mp_obj_is_true(args[0]));
        if (!mp_obj_is_true(args[0])) {
            do {
                mp_hal_delay_ms(2);
            } while (!mperror_heartbeat_disable_done());
        }
    } else {
        return mp_obj_new_bool(mperror_is_heartbeat_enabled());
    }
#else
    nlr_raise(mp_obj_new_exception_msg(&mp_type_OSError, "RGB Led Interface Disabled"));
#endif
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_pycom_heartbeat_obj, 0, 1, mod_pycom_heartbeat);

STATIC mp_obj_t mod_pycom_rgb_led (mp_obj_t o_color) {
#ifndef RGB_LED_DISABLE
    if (mperror_is_heartbeat_enabled()) {
        nlr_raise(mp_obj_new_exception_msg(&mp_type_OSError, mpexception_os_request_not_possible));
    }

    uint32_t color = mp_obj_get_int(o_color);
    led_info.color.value = color;
    led_set_color(&led_info, true, false);
#else
    nlr_raise(mp_obj_new_exception_msg(&mp_type_OSError, "RGB Led Interface Disabled"));
#endif

    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_pycom_rgb_led_obj, mod_pycom_rgb_led);

STATIC mp_obj_t mod_pycom_ota_start (void) {
    if (!updater_start()) {
        nlr_raise(mp_obj_new_exception_msg(&mp_type_OSError, mpexception_os_operation_failed));
    }
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_pycom_ota_start_obj, mod_pycom_ota_start);

STATIC mp_obj_t mod_pycom_ota_write (mp_obj_t data) {
    mp_buffer_info_t bufinfo;
    mp_get_buffer_raise(data, &bufinfo, MP_BUFFER_READ);

    if (!updater_write(bufinfo.buf, bufinfo.len)) {
        nlr_raise(mp_obj_new_exception_msg(&mp_type_OSError, mpexception_os_operation_failed));
    }
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_pycom_ota_write_obj, mod_pycom_ota_write);

STATIC mp_obj_t mod_pycom_ota_finish (void) {
    if (!updater_finish()) {
        nlr_raise(mp_obj_new_exception_msg(&mp_type_OSError, mpexception_os_operation_failed));
    }
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_pycom_ota_finish_obj, mod_pycom_ota_finish);

STATIC mp_obj_t mod_pycom_ota_verify (void) {
    bool ret_val = updater_verify();
    return mp_obj_new_bool(ret_val);
}
STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_pycom_ota_verify_obj, mod_pycom_ota_verify);

STATIC mp_obj_t mod_pycom_ota_slot (void) {
    int ota_slot = updater_ota_next_slot_address();
    return mp_obj_new_int(ota_slot);
}
STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_pycom_ota_slot_obj, mod_pycom_ota_slot);

STATIC mp_obj_t mod_pycom_pulses_get (mp_obj_t gpio, mp_obj_t timeout) {
    rmt_config_t rmt_rx;
    rmt_rx.channel = RMT_CHANNEL_0;
    rmt_rx.gpio_num = pin_find(gpio)->pin_number;
    rmt_rx.clk_div = 80;
    rmt_rx.mem_block_num = 1;
    rmt_rx.rmt_mode = RMT_MODE_RX;
    rmt_rx.rx_config.filter_en = true;
    rmt_rx.rx_config.filter_ticks_thresh = 100;
    rmt_rx.rx_config.idle_threshold = 0xFFFF;
    rmt_config(&rmt_rx);

    RingbufHandle_t rb = NULL;
    mp_obj_t pulses_l = mp_obj_new_list(0, NULL);

    // get the RMT RX ringbuffer
    rmt_get_ringbuf_handle(RMT_CHANNEL_0, &rb);
    rmt_rx_start(RMT_CHANNEL_0, 1);

    size_t rx_size = 0;
    rmt_item32_t* item = (rmt_item32_t*) xRingbufferReceive(rb, &rx_size, mp_obj_get_int(timeout));
    if (item) {
        for (int i = 0; i < rx_size / 4; i++) {
            mp_obj_t tuple[2];
            tuple[0] = mp_obj_new_int(item[i].level0);
            tuple[1] = mp_obj_new_int(item[i].duration0);
            mp_obj_list_append(pulses_l, mp_obj_new_tuple(2, tuple));

            tuple[0] = mp_obj_new_int(item[i].level1);
            tuple[1] = mp_obj_new_int(item[i].duration1);
            mp_obj_list_append(pulses_l, mp_obj_new_tuple(2, tuple));
        }

        // after parsing the data, return spaces to ringbuffer.
        vRingbufferReturnItem(rb, (void*) item);
    }

    rmt_rx_stop(RMT_CHANNEL_0);

    return pulses_l;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_pycom_pulses_get_obj, mod_pycom_pulses_get);

STATIC mp_obj_t mod_pycom_nvs_set (mp_obj_t _key, mp_obj_t _value) {

    const char *key = mp_obj_str_get_str(_key);
    esp_err_t esp_err = ESP_OK;

    if (MP_OBJ_IS_STR_OR_BYTES(_value)) {
        const char *value = mp_obj_str_get_str(_value);
        if (strlen(value) >= 1984) {
            // Maximum length (including null character) can be 1984 bytes
            nlr_raise(mp_obj_new_exception_msg(&mp_type_ValueError, "value too long (max: 1984)"));
        }
        esp_err = nvs_set_str(pycom_nvs_handle, key, value);
    } else if(MP_OBJ_IS_INT(_value)) {
        uint32_t value = mp_obj_get_int_truncated(_value);
        esp_err = nvs_set_u32(pycom_nvs_handle, key, value);
    } else {
        nlr_raise(mp_obj_new_exception_msg(&mp_type_ValueError, "Value must be string, bytes or integer"));
    }

    if (ESP_OK == esp_err) {
        nvs_commit(pycom_nvs_handle);
    } else if (ESP_ERR_NVS_NOT_ENOUGH_SPACE == esp_err || ESP_ERR_NVS_PAGE_FULL == esp_err || ESP_ERR_NVS_NO_FREE_PAGES == esp_err) {
        nlr_raise(mp_obj_new_exception_msg(&mp_type_OSError, "No free space available"));
    } else if (ESP_ERR_NVS_INVALID_NAME == esp_err || ESP_ERR_NVS_KEY_TOO_LONG == esp_err) {
        nlr_raise(mp_obj_new_exception_msg(&mp_type_ValueError, "Key is invalid"));
    } else {
        nlr_raise(mp_obj_new_exception_msg_varg(&mp_type_Exception, "Error occurred while storing value, code: %d", esp_err));
    }
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_pycom_nvs_set_obj, mod_pycom_nvs_set);

STATIC mp_obj_t mod_pycom_nvs_get (mp_uint_t n_args, const mp_obj_t *args) {

    const char *key = mp_obj_str_get_str(args[0]);
    esp_err_t esp_err = ESP_OK;
    mp_obj_t ret = mp_const_none;
    uint32_t value;

    esp_err = nvs_get_u32(pycom_nvs_handle, key, &value);
    if (esp_err == ESP_OK) {
        ret = mp_obj_new_int(value);
    }
    else {
        esp_err = nvs_get_str(pycom_nvs_handle, key, NULL, &value);
        if(esp_err == ESP_OK) {
            char* value_string = (char*)malloc(value);

            esp_err = nvs_get_str(pycom_nvs_handle, key, value_string, &value);

            if(esp_err == ESP_OK) {
                //do not count the terminating \0
                ret = mp_obj_new_str(value_string, value-1);
            }
            free(value_string);
        }
    }

    if(esp_err == ESP_ERR_NVS_NOT_FOUND) {
        if (n_args > 1) {
            // return user defined NoExistValue
            return args[1];
        }
        else
        {
            nlr_raise(mp_obj_new_exception_msg(&mp_type_ValueError, "No matching object for the provided key"));
        }
    } else if(esp_err != ESP_OK) {
        nlr_raise(mp_obj_new_exception_msg_varg(&mp_type_Exception, "Error occurred while fetching value, code: %d", esp_err));
    }

    return ret;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_pycom_nvs_get_obj, 1, 2, mod_pycom_nvs_get);

STATIC mp_obj_t mod_pycom_nvs_erase (mp_obj_t _key) {
    const char *key = mp_obj_str_get_str(_key);

    if (ESP_ERR_NVS_NOT_FOUND == nvs_erase_key(pycom_nvs_handle, key)) {
        nlr_raise(mp_obj_new_exception_msg(&mp_type_KeyError, "key not found"));
    }
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_pycom_nvs_erase_obj, mod_pycom_nvs_erase);

STATIC mp_obj_t mod_pycom_nvs_erase_all (void) {
    if (ESP_OK != nvs_erase_all(pycom_nvs_handle)) {
        nlr_raise(mp_obj_new_exception_msg(&mp_type_OSError, mpexception_os_operation_failed));
    }
    nvs_commit(pycom_nvs_handle);
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_pycom_nvs_erase_all_obj, mod_pycom_nvs_erase_all);

STATIC mp_obj_t mod_pycom_wifi_on_boot (mp_uint_t n_args, const mp_obj_t *args) {
    if (n_args == 0) {
        return mp_obj_new_bool(config_get_wifi_on_boot());
    }
    if (config_get_wifi_on_boot() != mp_obj_is_true(args[0])) {
        config_set_wifi_on_boot (mp_obj_is_true(args[0]));
        if (n_args > 1 && mp_obj_is_true(args[1])) {
            if (mp_obj_is_true(args[0])) {
                mptask_config_wifi(true);
            } else {
                wlan_deinit(NULL);
            }
        }
    }
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_pycom_wifi_on_boot_obj, 0, 2, mod_pycom_wifi_on_boot);

STATIC mp_obj_t mod_pycom_wifi_mode (mp_uint_t n_args, const mp_obj_t *args) {
    uint8_t mode;
    if (n_args) {
        mode = mp_obj_get_int(args[0]);
        switch(mode)
        {
            case 0:
                config_set_wifi_mode ((uint8_t)PYCOM_WIFI_CONF_MODE_NONE, true);
                break;
            case 1:
                config_set_wifi_mode ((uint8_t)PYCOM_WIFI_CONF_MODE_STA, true);
                break;
            case 2:
                config_set_wifi_mode ((uint8_t)PYCOM_WIFI_CONF_MODE_AP, true);
                break;
            case 3:
                config_set_wifi_mode ((uint8_t)PYCOM_WIFI_CONF_MODE_APSTA, true);
                break;
            default:
                break;
        }
    } else {
        mode = config_get_wifi_mode();
        switch(mode)
        {
            case PYCOM_WIFI_CONF_MODE_STA:
                return MP_OBJ_NEW_SMALL_INT(1);
            case PYCOM_WIFI_CONF_MODE_AP:
                return MP_OBJ_NEW_SMALL_INT(2);
            case PYCOM_WIFI_CONF_MODE_APSTA:
                return MP_OBJ_NEW_SMALL_INT(3);
            case PYCOM_WIFI_CONF_MODE_NONE:
            default:
                return MP_OBJ_NEW_SMALL_INT(0);
        }
    }
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_pycom_wifi_mode_obj, 0, 1, mod_pycom_wifi_mode);

STATIC mp_obj_t mod_pycom_wifi_ssid_sta (mp_uint_t n_args, const mp_obj_t *args) {
    if (n_args) {
        if(args[0] == mp_const_none)
        {
            config_set_sta_wifi_ssid (NULL, true);
        }
        else if (MP_OBJ_IS_STR(args[0]))
        {
            config_set_sta_wifi_ssid ((uint8_t *)(mp_obj_str_get_str(args[0])), true);
        }
        else{/*Nothing*/}

    } else {
        uint8_t * ssid = (uint8_t *)malloc(33);
        mp_obj_t ssid_obj;
        if(config_get_wifi_sta_ssid(ssid))
        {
            ssid_obj = mp_obj_new_str((const char *)ssid, strlen((const char *)ssid));
        }
        else
        {
            ssid_obj = mp_const_none;
        }
        free(ssid);
        return ssid_obj;
    }
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_pycom_wifi_ssid_sta_obj, 0, 1, mod_pycom_wifi_ssid_sta);

STATIC mp_obj_t mod_pycom_wifi_pwd_sta (mp_uint_t n_args, const mp_obj_t *args) {
    if (n_args) {
        if(args[0] == mp_const_none)
        {
            config_set_wifi_sta_pwd (NULL, true);
        }
        else if (MP_OBJ_IS_STR(args[0]))
        {
            config_set_wifi_sta_pwd ((uint8_t *)(mp_obj_str_get_str(args[0])), true);
        }
        else{/*Nothing*/}
    } else {
        uint8_t * pwd = (uint8_t *)malloc(65);
        mp_obj_t pwd_obj;
        if(config_get_wifi_sta_pwd(pwd))
        {
            pwd_obj = mp_obj_new_str((const char *)pwd, strlen((const char *)pwd));
        }
        else
        {
            pwd_obj = mp_const_none;
        }
        free(pwd);
        return pwd_obj;
    }
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_pycom_wifi_pwd_sta_obj, 0, 1, mod_pycom_wifi_pwd_sta);

STATIC mp_obj_t mod_pycom_wifi_ssid_ap (mp_uint_t n_args, const mp_obj_t *args) {
    if (n_args) {
        if(args[0] == mp_const_none)
        {
            config_set_wifi_ap_ssid (NULL);
        }
        else if (MP_OBJ_IS_STR(args[0]))
        {
            config_set_wifi_ap_ssid ((uint8_t *)(mp_obj_str_get_str(args[0])));
        }
        else{/*Nothing*/}
    } else {
        uint8_t * ssid = (uint8_t *)malloc(33);
        mp_obj_t ssid_obj;
        if(config_get_wifi_ap_ssid(ssid))
        {
            ssid_obj = mp_obj_new_str((const char *)ssid, strlen((const char *)ssid));
        }
        else
        {
            ssid_obj = mp_const_none;
        }
        free(ssid);
        return ssid_obj;
    }
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_pycom_wifi_ssid_ap_obj, 0, 1, mod_pycom_wifi_ssid_ap);

STATIC mp_obj_t mod_pycom_wifi_pwd_ap (mp_uint_t n_args, const mp_obj_t *args) {
    if (n_args) {
        if(args[0] == mp_const_none)
        {
            config_set_wifi_ap_pwd (NULL);
        }
        else if (MP_OBJ_IS_STR(args[0]))
        {
            config_set_wifi_ap_pwd ((uint8_t *)(mp_obj_str_get_str(args[0])));
        }
        else{/*Nothing*/}
    } else {
        uint8_t * pwd = (uint8_t *)malloc(65);
        mp_obj_t pwd_obj;
        if(config_get_wifi_ap_pwd(pwd))
        {
            pwd_obj = mp_obj_new_str((const char *)pwd, strlen((const char *)pwd));
        }
        else
        {
            pwd_obj = mp_const_none;
        }
        free(pwd);
        return pwd_obj;
    }
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_pycom_wifi_pwd_ap_obj, 0, 1, mod_pycom_wifi_pwd_ap);

STATIC mp_obj_t mod_pycom_wdt_on_boot (mp_uint_t n_args, const mp_obj_t *args) {
    if (n_args) {
        config_set_wdt_on_boot (mp_obj_is_true(args[0]));
    } else {
        return mp_obj_new_bool(config_get_wdt_on_boot());
    }
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_pycom_wdt_on_boot_obj, 0, 1, mod_pycom_wdt_on_boot);

STATIC mp_obj_t mod_pycom_wdt_on_boot_timeout (mp_uint_t n_args, const mp_obj_t *args) {
    if (n_args) {
        uint32_t timeout_ms = mp_obj_get_int(args[0]);
        if (timeout_ms < WDT_ON_BOOT_MIN_TIMEOUT_MS) {
            nlr_raise(mp_obj_new_exception_msg(&mp_type_ValueError, "WDT on boot timeout must be >= 5000 ms"));
        }
        config_set_wdt_on_boot_timeout (timeout_ms);
    } else {
        return mp_obj_new_int_from_uint(config_get_wdt_on_boot_timeout());
    }
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_pycom_wdt_on_boot_timeout_obj, 0, 1, mod_pycom_wdt_on_boot_timeout);

STATIC mp_obj_t mod_pycom_heartbeat_on_boot (mp_uint_t n_args, const mp_obj_t *args) {
    if (n_args) {
        config_set_heartbeat_on_boot (mp_obj_is_true(args[0]));
    } else {
        return mp_obj_new_bool(config_get_heartbeat_on_boot());
    }
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_pycom_heartbeat_on_boot_obj, 0, 1, mod_pycom_heartbeat_on_boot);

STATIC mp_obj_t mod_pycom_lte_modem_on_boot (mp_uint_t n_args, const mp_obj_t *args) {
    if (n_args) {
        config_set_lte_modem_enable_on_boot (mp_obj_is_true(args[0]));
    } else {
        return mp_obj_new_bool(config_get_lte_modem_enable_on_boot());
    }
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_pycom_lte_modem_on_boot_obj, 0, 1, mod_pycom_lte_modem_on_boot);

STATIC mp_obj_t mod_pycom_pybytes_on_boot (mp_uint_t n_args, const mp_obj_t *args) {
    if (n_args) {
        config_set_pybytes_autostart (mp_obj_is_true(args[0]));
    } else {
        return mp_obj_new_bool(config_get_pybytes_autostart());
    }
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_pycom_pybytes_on_boot_obj, 0, 1, mod_pycom_pybytes_on_boot);

STATIC mp_obj_t mod_pycom_pybytes_lte_config (size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
    enum { ARG_carrier, ARG_apn, ARG_cid, ARG_band, ARG_type, ARG_reset };
    STATIC const mp_arg_t allowed_args[] = {
            { MP_QSTR_carrier,          MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} },
            { MP_QSTR_apn,              MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} },
            { MP_QSTR_cid,              MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} },
            { MP_QSTR_band,             MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} },
            { MP_QSTR_type,             MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} },
            { MP_QSTR_reset,            MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} },

    };

    mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
    mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);

    pycom_pybytes_lte_config_t pycom_pybytes_lte_config = config_get_pybytes_lte_config();

    if (n_args == 0) {
        mp_obj_tuple_t *t = MP_OBJ_TO_PTR(mp_obj_new_tuple(6, NULL));

        if(pycom_pybytes_lte_config.carrier[0] == 0xFF && pycom_pybytes_lte_config.carrier[1] == 0xFF && pycom_pybytes_lte_config.carrier[2] == 0xFF)
        {
            t->items[ARG_carrier] = mp_const_none;
        }
        else
        {
            t->items[ARG_carrier] = mp_obj_new_str((const char *)pycom_pybytes_lte_config.carrier, strlen((const char *)pycom_pybytes_lte_config.carrier));
        }

        if(pycom_pybytes_lte_config.apn[0] == 0xFF && pycom_pybytes_lte_config.apn[1] == 0xFF && pycom_pybytes_lte_config.apn[2] == 0xFF)
        {
            t->items[ARG_apn] = mp_const_none;
        }
        else
        {
            t->items[ARG_apn] = mp_obj_new_str((const char *)pycom_pybytes_lte_config.apn, strlen((const char *)pycom_pybytes_lte_config.apn));
        }
        if(pycom_pybytes_lte_config.cid == 0xFF)
        {
            t->items[ARG_cid] = mp_obj_new_int(1);
        }
        else
        {
            t->items[ARG_cid] = mp_obj_new_int(pycom_pybytes_lte_config.cid);
        }
        if(pycom_pybytes_lte_config.band == 0xFF)
        {
            t->items[ARG_band] = mp_const_none;
        }
        else
        {
            t->items[ARG_band] = mp_obj_new_int(pycom_pybytes_lte_config.band);
        }
        if(pycom_pybytes_lte_config.type[0] == 0xFF)
        {
            t->items[ARG_type] = mp_const_none;
        }
        else
        {
            t->items[ARG_type] = mp_obj_new_str((const char *)pycom_pybytes_lte_config.type, strlen((const char *)pycom_pybytes_lte_config.type));
        }
        if(pycom_pybytes_lte_config.reset == 0xff)
        {
            t->items[ARG_reset] = mp_const_false;
        }
        else
        {
            t->items[ARG_reset] = mp_obj_new_bool(pycom_pybytes_lte_config.reset);
        }
        return MP_OBJ_FROM_PTR(t);

    } else {
        nlr_raise(mp_obj_new_exception_msg(&mp_type_ValueError, "Error this functionality is not yet supported!"));
    }

    return mp_const_none;
}

STATIC MP_DEFINE_CONST_FUN_OBJ_KW(mod_pycom_pybytes_lte_config_obj, 0, mod_pycom_pybytes_lte_config);

STATIC mp_obj_t mod_pycom_bootmgr (size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
    enum { ARG_boot_partition, ARG_fs_type, ARG_safeboot, ARG_status };
    STATIC const mp_arg_t allowed_args[] = {
            { MP_QSTR_boot_partition,   MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 255} },
            { MP_QSTR_fs_type,          MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 255} },
            { MP_QSTR_safeboot,         MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 255} },
            { MP_QSTR_reset,            MP_ARG_KW_ONLY | MP_ARG_BOOL, {.u_bool = false} },
    };

    mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
    mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);

    if (args[ARG_boot_partition].u_int == 255 && args[ARG_fs_type].u_int == 255 && args[ARG_safeboot].u_int == 255) {
        mp_obj_tuple_t *t = MP_OBJ_TO_PTR(mp_obj_new_tuple(4, NULL));

        if(boot_info.ActiveImg == 0x00)
        {
            t->items[ARG_boot_partition] = mp_obj_new_str("Factory", strlen("Factory"));
        }
        else
        {
            t->items[ARG_boot_partition] = mp_obj_new_str("ota_0", strlen("ota_0"));
        }

        if(config_get_boot_fs_type() == 0x00)
        {
            t->items[ARG_fs_type] = mp_obj_new_str("FAT", strlen("FAT"));
        }
        else
        {
            t->items[ARG_fs_type] = mp_obj_new_str("LittleFS", strlen("LittleFS"));
        }

        if(boot_info.safeboot == 0x00)
        {
            t->items[ARG_safeboot] = mp_obj_new_str("SafeBoot: False", strlen("SafeBoot: False"));
        }
        else
        {
            t->items[ARG_safeboot] = mp_obj_new_str("SafeBoot: True", strlen("SafeBoot: True"));
        }
        if(boot_info.Status == 0x00)
        {
            t->items[ARG_status] = mp_obj_new_str("Status: Check", strlen("Status: Check"));
        }
        else
        {
            t->items[ARG_status] = mp_obj_new_str("Status: Ready", strlen("Status: Ready"));
        }

        return MP_OBJ_FROM_PTR(t);

    } else {
        modpycom_bootmgr(args[ARG_boot_partition].u_int, args[ARG_fs_type].u_int, args[ARG_safeboot].u_int, args[3].u_bool);
    }

    return mp_const_none;
}

STATIC MP_DEFINE_CONST_FUN_OBJ_KW(mod_pycom_bootmgr_obj, 0, mod_pycom_bootmgr);

STATIC mp_obj_t mod_pycom_get_free_heap (void) {

    size_t heap_psram_free = 0;
    mp_obj_t items[2];

    if (esp32_get_chip_rev() > 0) {
        heap_psram_free = heap_caps_get_free_size(MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
    }
    items[0] = mp_obj_new_int(heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT));
    if(heap_psram_free)
    {
        items[1] = mp_obj_new_int(heap_psram_free);
    }
    else
    {
        items[1] = mp_obj_new_str("NO_EXT_RAM", strlen("NO_EXT_RAM"));
    }

    return mp_obj_new_tuple(2, items);
}
STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_pycom_get_free_heap_obj, mod_pycom_get_free_heap);

#if (VARIANT == PYBYTES)

STATIC mp_obj_t mod_pycom_pybytes_device_token (void) {
    uint8_t pybytes_device_token[39];
    config_get_pybytes_device_token(pybytes_device_token);
    return mp_obj_new_str((const char*)pybytes_device_token,strlen((const char*)pybytes_device_token));
}
STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_pycom_pybytes_device_token_obj, mod_pycom_pybytes_device_token);

STATIC mp_obj_t mod_pycom_pybytes_mqttServiceAddress (void) {
    uint8_t pybytes_mqttServiceAddress[39];
    config_get_pybytes_mqttServiceAddress(pybytes_mqttServiceAddress);
    return mp_obj_new_str((const char*)pybytes_mqttServiceAddress,strlen((const char*)pybytes_mqttServiceAddress));
}
STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_pycom_pybytes_mqttServiceAddress_obj, mod_pycom_pybytes_mqttServiceAddress);

STATIC mp_obj_t mod_pycom_pybytes_userId (void) {
    uint8_t pybytes_userId[254];
    config_get_pybytes_userId(pybytes_userId);
    return mp_obj_new_str((const char*)pybytes_userId,strlen((const char*)pybytes_userId));
}
STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_pycom_pybytes_userId_obj, mod_pycom_pybytes_userId);

STATIC mp_obj_t mod_pycom_pybytes_network_preferences (void) {
    uint8_t pybytes_network_preferences[54];
    config_get_pybytes_network_preferences(pybytes_network_preferences);
    return mp_obj_new_str((const char*)pybytes_network_preferences,strlen((const char*)pybytes_network_preferences));
}
STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_pycom_pybytes_network_preferences_obj, mod_pycom_pybytes_network_preferences);

STATIC mp_obj_t mod_pycom_pybytes_extra_preferences (void) {
    uint8_t pybytes_extra_preferences[99];
    config_get_pybytes_extra_preferences(pybytes_extra_preferences);
    return mp_obj_new_str((const char*)pybytes_extra_preferences,strlen((const char*)pybytes_extra_preferences));
}
STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_pycom_pybytes_extra_preferences_obj, mod_pycom_pybytes_extra_preferences);

STATIC mp_obj_t mod_pycom_pybytes_force_update (mp_uint_t n_args, const mp_obj_t *args) {
    if (n_args) {
        config_set_pybytes_force_update (mp_obj_is_true(args[0]));
    } else {
        return mp_obj_new_bool(config_get_pybytes_force_update());
    }
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_pycom_pybytes_force_update_obj, 0, 1, mod_pycom_pybytes_force_update);

STATIC mp_obj_t mod_pycom_smartConfig (mp_uint_t n_args, const mp_obj_t *args) {
    if (n_args) {
        config_set_wifi_smart_config ((uint8_t)(mp_obj_is_true(args[0])), true);
    } else {
        return mp_obj_new_bool(config_get_wifi_smart_config());
    }
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_pycom_smartConfig_obj, 0, 1, mod_pycom_smartConfig);

#endif //(VARIANT == PYBYTES)

// Helper function to return decimal value of a hexadecimal character coded in ASCII
STATIC uint8_t hex_from_char(const char c) {

    if((uint8_t)c >= '0' && (uint8_t)c <= '9') {
        return c - '0';
    }
    else if((uint8_t)c >= 'A' && (uint8_t)c <= 'F') {
        return c - ('A' - 10);
    }
    else if((uint8_t)c >= 'a' && (uint8_t)c <= 'f') {
        return c - ('a' - 10);
    }
    else {
        // 16 is invalid, because in hexa allowed range is 0 - 15
        return 16;
    }

}

STATIC mp_obj_t mod_pycom_sigfox_info (size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
    enum { ARG_id, ARG_pac, ARG_public_key, ARG_private_key, ARG_force };
    STATIC const mp_arg_t allowed_args[] = {
            { MP_QSTR_id,           MP_ARG_KW_ONLY  | MP_ARG_OBJ,  {.u_obj = mp_const_none} },
            { MP_QSTR_pac,          MP_ARG_KW_ONLY  | MP_ARG_OBJ,  {.u_obj = mp_const_none} },
            { MP_QSTR_public_key,   MP_ARG_KW_ONLY  | MP_ARG_OBJ,  {.u_obj = mp_const_none} },
            { MP_QSTR_private_key,  MP_ARG_KW_ONLY  | MP_ARG_OBJ,  {.u_obj = mp_const_none} },
            { MP_QSTR_force,        MP_ARG_KW_ONLY  | MP_ARG_BOOL, {.u_bool = false} }
    };
    mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
    mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);

    uint8_t id[4];
    uint8_t pac[8];
    uint8_t public_key[16];
    uint8_t private_key[16];

    config_get_sigfox_id(id);
    config_get_sigfox_pac(pac);
    config_get_sigfox_public_key(public_key);
    config_get_sigfox_private_key(private_key);

    if (args[ARG_id].u_obj == mp_const_none && args[ARG_pac].u_obj == mp_const_none && args[ARG_public_key].u_obj == mp_const_none && args[ARG_private_key].u_obj == mp_const_none) {
        // query sigfox info

        if ( !is_empty(id, sizeof(id)) && !is_empty(pac, sizeof(pac)) && !is_empty(public_key, sizeof(public_key)) && !is_empty(private_key, sizeof(private_key)) ){
            // all configs valid
            return mp_const_true;
        } else {
            return mp_const_false;
        }
    } else {
        // write sigfox info

        // temporary array to store even the longest value from id, pac, public_key and private_key
        uint8_t tmp_array[16];
        size_t length;
        bool ret_val = false;

        if (args[ARG_id].u_obj != mp_const_none) {

            if ( args[ARG_force].u_bool == true || is_empty(id, sizeof(id)) ) {

                const char* id = mp_obj_str_get_data(args[ARG_id].u_obj, &length);

                if(length != 8) {
                    nlr_raise(mp_obj_new_exception_msg(&mp_type_ValueError, "ID must have length of 8!"));
                }

                // Put together every 2 characters of the string (which are digits from 0 - 9 and a/A - f/F) into 1 bytes because the available space is half of the required one
                for(int i = 0, j = 0; i < length; i = i+2) {
                    uint8_t lower_nibble = hex_from_char(id[i+1]);
                    uint8_t upper_nibble = hex_from_char(id[i]);

                    if(lower_nibble == 16 || upper_nibble == 16) {
                        nlr_raise(mp_obj_new_exception_msg(&mp_type_ValueError, "ID must only contain hexadecimal digits!"));
                    }

                    tmp_array[j] = lower_nibble | (upper_nibble << 4);
                    j++;
                }

                ret_val = config_set_sigfox_id(tmp_array);
                if (ret_val == false) {
                    nlr_raise(mp_obj_new_exception_msg(&mp_type_ValueError, "Failed to write id"));
                }
            } else {
                nlr_raise(mp_obj_new_exception_msg(&mp_type_ValueError, "Use force option to overwrite existing id!"));
            }
        }
        if (args[ARG_pac].u_obj != mp_const_none) {

            if (args[ARG_force].u_bool == true || is_empty(pac, sizeof(pac))) {

                const char* pac = mp_obj_str_get_data(args[ARG_pac].u_obj, &length);

                if(length != 16) {
                    nlr_raise(mp_obj_new_exception_msg(&mp_type_ValueError, "PAC must have length of 16!"));
                }

                // Put together every 2 characters of the string (which are digits from 0 - 9 and a/A - f/F) into 1 bytes because the available space is half of the required one
                for(int i = 0, j = 0; i < length; i = i+2) {
                    uint8_t lower_nibble = hex_from_char(pac[i+1]);
                    uint8_t upper_nibble = hex_from_char(pac[i]);

                    if(lower_nibble == 16 || upper_nibble == 16) {
                        nlr_raise(mp_obj_new_exception_msg(&mp_type_ValueError, "PAC must only contain hexadecimal digits!"));
                    }

                    tmp_array[j] = lower_nibble | (upper_nibble << 4);
                    j++;
                }

                ret_val = config_set_sigfox_pac(tmp_array);
                if (ret_val == false) {
                    nlr_raise(mp_obj_new_exception_msg(&mp_type_ValueError, "Failed to write pac"));
                }
            } else {
                nlr_raise(mp_obj_new_exception_msg(&mp_type_ValueError, "Use force option to overwrite existing pac!"));
            }
        }
        if (args[ARG_public_key].u_obj != mp_const_none) {

            if (args[ARG_force].u_bool == true || is_empty(public_key, sizeof(public_key))) {

                const char* public_key = mp_obj_str_get_data(args[ARG_public_key].u_obj, &length);

                if(length != 32) {
                    nlr_raise(mp_obj_new_exception_msg(&mp_type_ValueError, "Public Key must have length of 32!"));
                }

                // Put together every 2 characters of the string (which are digits from 0 - 9 and a/A - f/F) into 1 bytes because the available space is half of the required one
                for(int i = 0, j = 0; i < length; i = i+2) {
                    uint8_t lower_nibble = hex_from_char(public_key[i+1]);
                    uint8_t upper_nibble = hex_from_char(public_key[i]);

                    if(lower_nibble == 16 || upper_nibble == 16) {
                        nlr_raise(mp_obj_new_exception_msg(&mp_type_ValueError, "Public Key must only contain hexadecimal digits!"));
                    }

                    tmp_array[j] = lower_nibble | (upper_nibble << 4);
                    j++;
                }

                ret_val = config_set_sigfox_public_key(tmp_array);
                if (ret_val == false) {
                    nlr_raise(mp_obj_new_exception_msg(&mp_type_ValueError, "Failed to write public key"));
                }
            } else {
                nlr_raise(mp_obj_new_exception_msg(&mp_type_ValueError, "Use force option to overwrite existing public key!"));
            }

        }
        if (args[ARG_private_key].u_obj != mp_const_none) {

            if (args[ARG_force].u_bool == true || is_empty(private_key, sizeof(private_key))) {

                const char* private_key = mp_obj_str_get_data(args[ARG_private_key].u_obj, &length);

                if(length != 32) {
                    nlr_raise(mp_obj_new_exception_msg(&mp_type_ValueError, "Private Key must have length of 32"));
                }

                // Put together every 2 characters of the string (which are digits from 0 - 9 and a/A - f/F) into 1 bytes because the available space is half of the required one
                for(int i = 0, j = 0; i < length; i = i+2) {
                    uint8_t lower_nibble = hex_from_char(private_key[i+1]);
                    uint8_t upper_nibble = hex_from_char(private_key[i]);

                    if(lower_nibble == 16 || upper_nibble == 16) {
                        nlr_raise(mp_obj_new_exception_msg(&mp_type_ValueError, "Private Key must only contain hexadecimal digits!"));
                    }

                    tmp_array[j] = lower_nibble | (upper_nibble << 4);
                    j++;
                }

                ret_val = config_set_sigfox_private_key(tmp_array);
                if (ret_val == false) {
                    nlr_raise(mp_obj_new_exception_msg(&mp_type_ValueError, "Failed to write private key"));
                }
            } else {
                nlr_raise(mp_obj_new_exception_msg(&mp_type_ValueError, "Use force option to overwrite existing private key!"));
            }
        }
        return mp_const_true;
    }
}
STATIC MP_DEFINE_CONST_FUN_OBJ_KW(mod_pycom_sigfox_info_obj, 0, mod_pycom_sigfox_info);

STATIC const mp_map_elem_t pycom_module_globals_table[] = {
        { MP_OBJ_NEW_QSTR(MP_QSTR___name__),                        MP_OBJ_NEW_QSTR(MP_QSTR_pycom) },
        { MP_OBJ_NEW_QSTR(MP_QSTR_heartbeat),                       (mp_obj_t)&mod_pycom_heartbeat_obj },
        { MP_OBJ_NEW_QSTR(MP_QSTR_rgbled),                          (mp_obj_t)&mod_pycom_rgb_led_obj },
        { MP_OBJ_NEW_QSTR(MP_QSTR_ota_start),                       (mp_obj_t)&mod_pycom_ota_start_obj },
        { MP_OBJ_NEW_QSTR(MP_QSTR_ota_write),                       (mp_obj_t)&mod_pycom_ota_write_obj },
        { MP_OBJ_NEW_QSTR(MP_QSTR_ota_finish),                      (mp_obj_t)&mod_pycom_ota_finish_obj },
        { MP_OBJ_NEW_QSTR(MP_QSTR_ota_verify),                      (mp_obj_t)&mod_pycom_ota_verify_obj },
        { MP_OBJ_NEW_QSTR(MP_QSTR_ota_slot),                        (mp_obj_t)&mod_pycom_ota_slot_obj },
        { MP_OBJ_NEW_QSTR(MP_QSTR_pulses_get),                      (mp_obj_t)&mod_pycom_pulses_get_obj },
        { MP_OBJ_NEW_QSTR(MP_QSTR_nvs_set),                         (mp_obj_t)&mod_pycom_nvs_set_obj },
        { MP_OBJ_NEW_QSTR(MP_QSTR_nvs_get),                         (mp_obj_t)&mod_pycom_nvs_get_obj },
        { MP_OBJ_NEW_QSTR(MP_QSTR_nvs_erase),                       (mp_obj_t)&mod_pycom_nvs_erase_obj },
        { MP_OBJ_NEW_QSTR(MP_QSTR_nvs_erase_all),                   (mp_obj_t)&mod_pycom_nvs_erase_all_obj },
        { MP_OBJ_NEW_QSTR(MP_QSTR_wifi_on_boot),                    (mp_obj_t)&mod_pycom_wifi_on_boot_obj },
        { MP_OBJ_NEW_QSTR(MP_QSTR_wdt_on_boot),                     (mp_obj_t)&mod_pycom_wdt_on_boot_obj },
        { MP_OBJ_NEW_QSTR(MP_QSTR_wdt_on_boot_timeout),             (mp_obj_t)&mod_pycom_wdt_on_boot_timeout_obj },
        { MP_OBJ_NEW_QSTR(MP_QSTR_heartbeat_on_boot),               (mp_obj_t)&mod_pycom_heartbeat_on_boot_obj },
        { MP_OBJ_NEW_QSTR(MP_QSTR_lte_modem_en_on_boot),            (mp_obj_t)&mod_pycom_lte_modem_on_boot_obj },
        { MP_OBJ_NEW_QSTR(MP_QSTR_get_free_heap),                   (mp_obj_t)&mod_pycom_get_free_heap_obj },
        { MP_OBJ_NEW_QSTR(MP_QSTR_wifi_ssid_sta),                   (mp_obj_t)&mod_pycom_wifi_ssid_sta_obj },
        { MP_OBJ_NEW_QSTR(MP_QSTR_wifi_ssid_ap),                    (mp_obj_t)&mod_pycom_wifi_ssid_ap_obj },
        { MP_OBJ_NEW_QSTR(MP_QSTR_wifi_pwd_sta),                    (mp_obj_t)&mod_pycom_wifi_pwd_sta_obj },
        { MP_OBJ_NEW_QSTR(MP_QSTR_wifi_pwd_ap),                     (mp_obj_t)&mod_pycom_wifi_pwd_ap_obj },
        { MP_OBJ_NEW_QSTR(MP_QSTR_wifi_mode_on_boot),               (mp_obj_t)&mod_pycom_wifi_mode_obj },

#if defined(FIPY) || defined(LOPY4) || defined(SIPY)
        { MP_OBJ_NEW_QSTR(MP_QSTR_sigfox_info),                     (mp_obj_t)&mod_pycom_sigfox_info_obj },
#endif

#if (VARIANT == PYBYTES)
        { MP_OBJ_NEW_QSTR(MP_QSTR_pybytes_device_token),            (mp_obj_t)&mod_pycom_pybytes_device_token_obj },
        { MP_OBJ_NEW_QSTR(MP_QSTR_pybytes_mqttServiceAddress),      (mp_obj_t)&mod_pycom_pybytes_mqttServiceAddress_obj },
        { MP_OBJ_NEW_QSTR(MP_QSTR_pybytes_userId),                  (mp_obj_t)&mod_pycom_pybytes_userId_obj },
        { MP_OBJ_NEW_QSTR(MP_QSTR_pybytes_network_preferences),     (mp_obj_t)&mod_pycom_pybytes_network_preferences_obj },
        { MP_OBJ_NEW_QSTR(MP_QSTR_pybytes_extra_preferences),       (mp_obj_t)&mod_pycom_pybytes_extra_preferences_obj },
        { MP_OBJ_NEW_QSTR(MP_QSTR_pybytes_force_update),            (mp_obj_t)&mod_pycom_pybytes_force_update_obj },
        { MP_OBJ_NEW_QSTR(MP_QSTR_smart_config_on_boot),            (mp_obj_t)&mod_pycom_smartConfig_obj },
        { MP_OBJ_NEW_QSTR(MP_QSTR_pybytes_lte_config),              (mp_obj_t)&mod_pycom_pybytes_lte_config_obj },
        { MP_OBJ_NEW_QSTR(MP_QSTR_pybytes_on_boot),                 (mp_obj_t)&mod_pycom_pybytes_on_boot_obj },

#endif //(VARIANT == PYBYTES)
        { MP_OBJ_NEW_QSTR(MP_QSTR_bootmgr),                         (mp_obj_t)&mod_pycom_bootmgr_obj },

        // class constants
        { MP_OBJ_NEW_QSTR(MP_QSTR_FACTORY),                         MP_OBJ_NEW_SMALL_INT(0) },
        { MP_OBJ_NEW_QSTR(MP_QSTR_OTA_0),                           MP_OBJ_NEW_SMALL_INT(1) },
        { MP_OBJ_NEW_QSTR(MP_QSTR_FAT),                           MP_OBJ_NEW_SMALL_INT(0) },
        { MP_OBJ_NEW_QSTR(MP_QSTR_LittleFS),                        MP_OBJ_NEW_SMALL_INT(1) },

};

STATIC MP_DEFINE_CONST_DICT(pycom_module_globals, pycom_module_globals_table);

const mp_obj_module_t pycom_module = {
        .base = { &mp_type_module },
        .globals = (mp_obj_dict_t*)&pycom_module_globals,
};


Kolya has a string s of length n consisting of lowercase and uppercase Latin letters and digits.

He wants to rearrange the symbols in s and cut it into the minimum number of parts so that each part is a palindrome and all parts have the same lengths. A palindrome is a string which reads the same backward as forward, such as madam or racecar.

Your task is to help Kolya and determine the minimum number of palindromes of equal lengths to cut s into, if it is allowed to rearrange letters in s before cuttings.


Payloadvalidatinginterceptor not working maks chmerkovskiy erin still dating
The message dispatcher operates on a message context, and not transport-specific input stream and output stream.
As a result, transport specific requests need to read into a Spring Web Services supports multiple transport protocols.
The following xml snippets shows an example: The first two bean definitions are straight forward, Spring WS offer other endpoint mapping strategies so feel free to explore.
The remoting module in the Core Spring Framework contains a convenient factory bean for the HTTP server: the Finally, Spring Web Services 2.0 introduced support for XMPP, otherwise known as Jabber. Spring Web Services support for XMPP is very similar to the other transports: there is a a Endpoints are the central concept in Spring-WS's server-side support.
Endpoints provide access to the application behavior which is typically defined by a business service interface.
An endpoint interprets the XML request message and uses that input to invoke a method on the business service (typically).
The result of that service invocation is represented as a response message.
The embedded HTTP Server is a standalone server that is simple to configure.


This is the solo version of the problem. Note that the solution of this problem may or may not share ideas with the solution of the game version. You can solve and get points for both versions independently.

You can make hacks only if both versions of the problem are solved.

Given an integer variable $$$x$$$ with the initial value of $$$n$$$. A single break operation consists of the following steps:

- Choose a value $$$y$$$ such that $$$0 \lt y \lt x$$$ and $$$0 \lt (x \oplus y) \lt x$$$.
- Update $$$x$$$ by either setting $$$x = y$$$ or setting $$$x = x \oplus y$$$.

You don't need to minimize the number of operations.

Here $$$\oplus$$$ denotes the bitwise XOR operation.


Pangea Software's Ultimate Game Programming Guide for Mac OS X
by Brian Greenstone
Publisher: Pangea Software, Inc 2004
Number of pages: 288
Brian Greenstone reveals his Mac programming secrets in this must-have book for any aspiring game programmer. The text covers all of the nuances of various Mac OS technologies such as the OpenGL, HID Manager, OpenAL, Rendezvous, Core Graphics, Quicktime, Maya plug-ins, stereo 3D rendering, AltiVec optimizations, networking, copy-protection of games, marketing and sales strategies for self-publishers.
Home page url
Download or read it online for free here:
by Albert Sweigart
A programming book for kids interested in learning to program their own computer games with python, a serious computer language which is used by professional programmers also. The book explains programming principles from the source code examples.
by Michael Morrison - InformIT
Learn how to use C++ code to create basic animated graphics in this chapter from Beginning Game Programming by Michael Morrison. Practical lesson based on C++ programming, the basics of drawing graphics using the Windows Graphics Device Interface.
by Antonio Hernandez Bejarano - GitBook
This online book will introduce the main concepts required to write a 3D game using the LWJGL 3 library. LWJGL is a Java library that provides access to native APIs used in the development of graphics, audio and parallel computing applications.
by Richard Fabian - dataorienteddesign.com
A practical guide for game developers working to create triple A titles across multiple platforms, for independent developers trying to get the most out of target hardware, for anyone who develops cutting edge software in restrictive hardware.


$$$n$$$ people live on the coordinate line, the $$$i$$$-th one lives at the point $$$x_i$$$ ($$$1 \le i \le n$$$). They want to choose a position $$$x_0$$$ to meet. The $$$i$$$-th person will spend $$$|x_i - x_0|$$$ minutes to get to the meeting place. Also, the $$$i$$$-th person needs $$$t_i$$$ minutes to get dressed, so in total he or she needs $$$t_i + |x_i - x_0|$$$ minutes.

Here $$$|y|$$$ denotes the absolute value of $$$y$$$.

These people ask you to find a position $$$x_0$$$ that minimizes the time in which all $$$n$$$ people can gather at the meeting place.


/******************************************************************************
 * The MIT License (MIT)
 *
 * Copyright (c) 2019-2022 Baldur Karlsson
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 ******************************************************************************/

#pragma once

#include <frameobject.h>
#include <atomic>

// this is defined elsewhere for managing the opaque global_handle object
extern "C" PyThreadState *GetExecutingThreadState(PyObject *global_handle);
extern "C" PyObject *GetCurrentGlobalHandle();
extern "C" void HandleException(PyObject *global_handle);
extern "C" bool IsThreadBlocking(PyObject *global_handle);
extern "C" void SetThreadBlocking(PyObject *global_handle, bool block);
extern "C" void QueueDecRef(PyObject *obj);
extern "C" void ProcessDecRefQueue();

struct ExceptionData
{
  bool failFlag = false;
  PyObject *exObj = NULL;
  PyObject *valueObj = NULL;
  PyObject *tracebackObj = NULL;
};

struct ExceptionHandler
{
  struct CreateTag
  {
  };
  explicit ExceptionHandler(CreateTag) : m_storage(new Storage) { m_storage->m_ref = 1; }
  ~ExceptionHandler()
  {
    if(m_storage)
    {
      m_storage->m_ref--;
      if(m_storage->m_ref == 0)
        delete m_storage;
    }
    m_storage = NULL;
  }
  ExceptionHandler(const ExceptionHandler &o) : m_storage(o.m_storage) { m_storage->m_ref++; }
  ExceptionHandler &operator=(const ExceptionHandler &o)
  {
    this->~ExceptionHandler();
    m_storage = o.m_storage;
    m_storage->m_ref++;
    return *this;
  }
  ExceptionData &data() { return m_storage->data; }
  const ExceptionData &data() const { return m_storage->data; }
  operator bool() const { return m_storage->valid; }
  void disconnect() { m_storage->valid = false; }
private:
  struct Storage
  {
    ExceptionData data;
    bool valid = true;
    std::atomic<int32_t> m_ref;
  };
  Storage *m_storage = NULL;
};

struct StackExceptionHandler
{
  StackExceptionHandler() : m_Handler(ExceptionHandler::CreateTag()) {}
  ~StackExceptionHandler() { m_Handler.disconnect(); }
  ExceptionData &data() { return m_Handler.data(); }
  operator ExceptionHandler() { return m_Handler; }
private:
  ExceptionHandler m_Handler;
};

// this function handles failures in callback functions. If we're synchronously calling the callback
// from within an execute scope, then we can assign to failflag and let the error propagate upwards.
// If we're not, then the callback is being executed on another thread with no knowledge of python,
// so we need to use the global handle to try and emit the exception through the context. None of
// this is multi-threaded because we're inside the GIL at all times
inline void HandleCallbackFailure(PyObject *global_handle, ExceptionHandler exHandle)
{
  // if there's no global handle assume we are not running in the usual environment, so there are no
  // external-to-python threads.
  // Specifically this is when we're imported as a module directly into python with none of our
  // harness, so this is running as pure glue code.
  if(!global_handle)
  {
    if(exHandle)
      exHandle.data().failFlag = true;
    else
      RENDERDOC_LogMessage(LogType::Error, "QTRD", __FILE__, __LINE__,
                           "Callback failure with no global handle and no valid parent scope!");
    return;
  }

  PyThreadState *current = PyGILState_GetThisThreadState();
  PyThreadState *executing = GetExecutingThreadState(global_handle);

  // we are executing synchronously and the exception handler is still valid, set the flag and
  // return to the parent scope where it exists and will handle the exception
  if(current == executing && exHandle)
  {
    exHandle.data().failFlag = true;
    return;
  }

  // if we have the blocking flag set, then we may be on another thread but we can still propagate
  // up the error
  if(IsThreadBlocking(global_handle))
  {
    if(exHandle)
    {
      exHandle.data().failFlag = true;

      // we need to rethrow the exception to that thread, so fetch (and clear it) on this thread.
      //
      // Note that the exception can only propagate up to one place. However since we know that
      // python is inherently single threaded, so if we're doing this blocking funciton call on
      // another thread then we *know* there isn't python further up the stack. Therefore we're safe
      // to swallow the exception here (since there's nowhere for it to bubble up to anyway) and
      // rethrow on the python thread.
      PyErr_Fetch(&exHandle.data().exObj, &exHandle.data().valueObj, &exHandle.data().tracebackObj);
    }
    return;
  }

  // in this case we are executing asynchronously, and must handle the exception manually as there's
  // nothing above us that knows about python exceptions
  HandleException(global_handle);
}

template <typename T>
inline T get_return(const char *funcname, PyObject *result, PyObject *global_handle,
                    ExceptionHandler exHandle)
{
  T val = T();

  int res = ConvertFromPy(result, val);

  if(!SWIG_IsOK(res))
  {
    HandleCallbackFailure(global_handle, exHandle);

    PyErr_Format(PyExc_TypeError, "Unexpected type for return value of callback in %s", funcname);
  }

  Py_XDECREF(result);

  return val;
}

template <>
inline void get_return(const char *funcname, PyObject *result, PyObject *global_handle,
                       ExceptionHandler exHandle)
{
  Py_XDECREF(result);
}

struct PyObjectRefCounter
{
  PyObjectRefCounter(PyObject *o) : obj(o) { Py_INCREF(obj); }
  PyObjectRefCounter(const PyObjectRefCounter &o)
  {
    obj = o.obj;
    Py_INCREF(obj);
  }
  ~PyObjectRefCounter()
  {
    // it may not be safe at the point this is destroyed to decref the object. For example if a
    // python lambda is passed into a C++ invoke function, we will be holding the only reference to
    // that lambda here when the async invoke completes and destroyed the std::function wrapping it.
    // Without python executing we can't decref it to 0. Instead we queue the decref, and it will be
    // done as soon as safely possible.
    if(PyGILState_Check() == 0)
      QueueDecRef(obj);
    else
      Py_DECREF(obj);
  }
  PyObject *obj;
};

template <typename rettype, typename... paramTypes>
struct varfunc
{
  varfunc(const char *funcname, paramTypes... params)
  {
    args = PyTuple_New(sizeof...(paramTypes));

    currentarg = 0;

    // avoid unused parameter errors when calling a parameter-less function
    (void)funcname;

    using expand_type = int[];
    (void)expand_type{0, (push_arg(funcname, params), 0)...};
  }

  template <typename T>
  void push_arg(const char *funcname, const T &arg)
  {
    if(!args)
      return;

    PyObject *obj = ConvertToPy(arg);

    if(!obj)
    {
      Py_DecRef(args);
      args = NULL;

      PyErr_Format(PyExc_TypeError, "Unexpected type for arg %d of callback in %s", currentarg + 1,
                   funcname);

      return;
    }

    PyTuple_SetItem(args, currentarg++, obj);
  }

  ~varfunc() { Py_XDECREF(args); }
  rettype call(const char *funcname, PyObject *func, PyObject *global_handle,
               ExceptionHandler exHandle)
  {
    if(!func || !PyCallable_Check(func) || !args)
    {
      HandleCallbackFailure(global_handle, exHandle);
      return rettype();
    }

    ProcessDecRefQueue();

    PyObject *result = PyObject_Call(func, args, 0);

    Py_DECREF(args);

    if(result == NULL)
    {
      HandleCallbackFailure(global_handle, exHandle);
      return rettype();
    }

    return get_return<rettype>(funcname, result, global_handle, exHandle);
  }

  int currentarg = 0;
  PyObject *args;
};

struct ScopedFuncCall
{
  ScopedFuncCall(PyObject *h)
  {
    handle = h;
    Py_XINCREF(handle);
    gil = PyGILState_Ensure();
  }

  ~ScopedFuncCall()
  {
    Py_XDECREF(handle);
    PyGILState_Release(gil);
  }

  PyObject *handle;
  PyGILState_STATE gil;
};

template <typename funcType>
funcType ConvertFunc(const char *funcname, PyObject *func, ExceptionHandler exHandle)
{
  // allow None to indicate no callback
  if(func == Py_None)
    return funcType();

  // add a reference to the global object so it stays alive while we execute, in case this is an
  // async call
  PyObject *global_internal_handle = NULL;

  // walk the frames until we find one with _renderdoc_internal. If we call a function in another
  // module the globals may not have the entry, but the root level is expected to.
  {
    _frame *frame = PyEval_GetFrame();

    while(frame)
    {
      global_internal_handle = PyDict_GetItemString(frame->f_globals, "_renderdoc_internal");

      if(global_internal_handle)
        break;
      frame = frame->f_back;
    }
  }

  if(!global_internal_handle)
    global_internal_handle = GetCurrentGlobalHandle();

  // process any dangling functions that may need to be cleared up
  ProcessDecRefQueue();

  // create a copy that will keep the function object alive as long as the lambda is
  PyObjectRefCounter funcptr(func);

  return [global_internal_handle, funcname, funcptr, exHandle](auto... param) {
    ScopedFuncCall gil(global_internal_handle);

    varfunc<typename funcType::result_type, decltype(param)...> f(funcname, param...);
    return f.call(funcname, funcptr.obj, global_internal_handle, exHandle);
  };
}


#import <Foundation/Foundation.h>

@interface NSURL (OVDirectories)

/** Returns an NSURL representing the first path found matching the specified constants or nil if none */
+ (NSURL *)URLForDirectory:(NSSearchPathDirectory)directoryConstant domainMask:(NSSearchPathDomainMask)domainMask;

/** Returns the application support directory with the app's bundle id appended.  As recommended in the Fil System Programming Guide */
+ (NSURL *)URLForApplicationSupportDataDirectory;

/** Append a subfolder/file path onto the app data directory */
+ (NSURL *)URLForApplicationSupportWithAppendedPath:(NSString *)pathToAppend;

/** Returns the user directory */
+ (NSURL *)URLForUserDirectory;

/** Append a subfolder/file path onto the user directory */
+ (NSURL *)URLForUserDirectoryWithAppendedPath:(NSString *)pathToAppend;

/** Returns the user's document directory */
+ (NSURL *)URLForDocumentDirectory;

/** Append a subfolder/file path onto the user's document directory */
+ (NSURL *)URLForDocumentDirectoryWithAppendedPath:(NSString *)pathToAppend;

/** Uses NSTemporaryDirectory() to get a URL to a temp folder */
+ (NSURL *)URLForTemporaryDirectory;

/** URL to a temporary subfolder and/or file */
+ (NSURL *)URLForTemporaryDirectoryWithAppendedPath:(NSString *)pathToAppend;

@end


sosley, I am getting the same warning: Warning: Missing argument 1 for BiblioCopy::validateData(), called in C:\xampp\htdocs\openbiblio\catalog\upload_csv.php on line 275 and defined in C:\xampp\htdocs\openbiblio\classes\BiblioCopy.php on line 43 It only happens when my .csv file includes a column for "barCo". When I remove that column, the file tests just fine. The problem is...
2013-05-22 05:42:48 PDT by michaeldroush
Thanks. Yes, adding the neccesary .layout line in the report file was indeed all that was wrong. I'll need to make a note of that somewhere for the next time I can't remember! For copies.rpt I'm using the "Copy Search (Extended)" from 0.6.1 rather than the one included with 0.7.1.
2013-05-06 20:05:44 PDT by https://www.google.com/accounts
[excerpt from direct communication] OpenBiblio downloads are offered through SourceForge and these are mirrored automatically. http://sourceforge.net/apps/trac/sourceforge/wiki/Mirrors Help is still appreciated, especially for answering messages in the forums and improving the documentation in our wiki.
2013-05-06 03:25:59 PDT by infinite-mnkz
Correct. An easy fix is overwriting the 0.7.1 .rpt files with the files from the 0.6.1. installation, except for the reports that were updated for 0.7.1 (copies.rpt, members.rpt, popularBiblios.rpt).
2013-05-06 03:18:28 PDT by infinite-mnkz
I think I may have found the problem. I had completely forgotten that the .rpt files need to be edited as well to include which printable layouts can be accessed after the report is run. Will try this.
2013-05-05 19:48:26 PDT by https://www.google.com/accounts
Update ... This is still stumping me. I thought for a second I had it figured out when I realized I had renamed my custom label file and then not changed the class name to match, but after editing that I still had no success. After I run a report, the only options for printing that will display under "Report Results" are from labels.php and list.php. Why can't I get anything else from...
2013-05-05 17:42:42 PDT by https://www.google.com/accounts
I just upgraded from 0.6.1 to 0.7.1 and now when I go to print labels from a report, no options are showing as before. I have a customized label file in ./layouts but neither that nor any of the included in ./layouts/default are showing in the menu. I sort of feel like I've been in this spot here before a year or two ago with the previous installation. Is there some configuration setting...
2013-05-04 18:37:25 PDT by https://www.google.com/accounts
Good afternoon, I have a home web server running Debian Stable with 250GB of disk space. I'm not going to be using all of that myself, so I'm wondering if the devs would mind if I use part of the drive to create a publically-accessible mirror of OpenBiblio. I have no knowledge of PHP or any kind of coding, so this seems to be the most obvious way I can help. I'm in southern California, if...
2013-05-04 12:58:14 PDT by zlanvok
Hi, This is for a not for profit organisation looking at serving it's members with an online library facility. We are looking for some customisation in the area of look and feel and few other things. Any one interested, please mail me to bapajan at yahoo dot co dot uk Regards.
2013-04-30 03:43:05 PDT by bapajan
Hello, Thank you for your support. I'm gonna try!
2013-04-22 02:15:37 PDT by llimoner


Windows Authentication in DTS package help
Posted on 2007-07-30
I have a DTS package on a SQL server scheduled to run as a Job. This package connects to a remote SQL server to grab data and update local tables. In the past, we had a SQL login for the remote server, and the connection in the DTS was configured using "SQL Server Authentication" with that ID.
Due to security requirements, no more SQL logins are allowed, and all connections have to be made using Windows Authentication. We created a new domain acccount and granted the necessary access on the remote and local servers.
The question is: How can I setup my DTS or Job to connect to that remote server using that domain ID? Changing the identity of my local SQL Agent would create all sorts of problems and require other changes that I prefer not to make. Also, in the future, I might need a different ID to connect to other remote databases. Is this possible? Please help.
Thanks in advance.


Yesterday my phone started buzzing more than usual with Twitter updates. Turns out our working paper on A/B testing has attracted the attention of a few notable academics on Twitter. Again.
I'm not sure what prompted this new wave of attention. Perhaps it was the fact we uploaded a new version to SSRN. Perhaps it is those end of year summaries I see people making.
One tweet, however, from Ron Kohavi, Microsoft VP of Analysis and Experimentation, was interesting:
Claim of widespread p-hacking sensationalized in paper suffering lack of external validity on two axes: skew due to massive selection bias and time bias.
— Ronny Kohavi (@ronnyk) December 16, 2018
You can see the linked(in) post here: https://www.linkedin.com/pulse/p-hacking-ab-testing-sensationalized-ronny-kohavi/
"Oh, dear", I was thinking to myself. Kohavi is one of the stars of the A/B testing world. What's up with that?
I will address Kohavi's points below, to the best of my understanding of the issues he raised. I am not sure what prompted this "sensational tweet", but I very much welcome feedback about our paper. I hear that email still works as a technology, though.
TL;DR: Don't be that person that doesn't read an entire post because it'll take you 3 minutes. You'll end up like those who make judgement calls about papers based on Tweets by other people:
1. "Selection bias" - Kohavi says "Optimizely’s product was designed for the uninformed, so there is massive selection bias." By "selection bias" Kohavi means that the sample of people in our data who use the platform to run experiments is not representative of the behavior of "normal" ("informed") experimenters.
The fact is that Optimizely is the largest (as far as I can tell) 3rd party A/B testing provider, which is why we worked very hard to convince them to share data with us. The median experimenter in our dataset had run 184 experiments before we collected our data, and there are over 900 of them in our dataset. The platform had over 9,000 experiments started on it in the data collection window. This is more than 6 times higher than what Kohavi mentions they ran on Bing in roughly the same time period (though I am sure Bing.com experiments are larger in sample size).
A study that analyzes experimenter behavior in the wild, on a widely used A/B testing platform, has a lot of external validity about how businesses behave when they experiment. The findings might not apply to Microsoft, or Facebook, but these companies do not need our help in analyzing their internal experiments. In contrast, deriving conclusions from studies about experiments that are run internally at Microsoft or Facebook probably apply less to a "normal" business.
2. "Selection bias" (2) - Even "uninformed" experimenters should have noticed if, experiment after experiment, their decisions result in lower profit. This is basically the rational decision maker assumption we often make in our analysis. We wanted to test it, and we found interesting results.
3. "Selection bias" (3) - Kohavi claims (or assumes) that informed decision makers do not make such mistakes.
In contrast to just claiming otherwise, our paper actually cites many references that show this is incorrect (medical doctors, informed academics and even statisticians make mistakes in interpreting results).
So to summarize - is our dataset representative? It is, if you're the type of A/B tester that runs experiments similar to those used on the platform we got the data from. Luckily, it applies to many if not most businesses.
4. "Time Bias" - Kohavi says "The paper’s external validity across time is therefore weak: most businesses who use A/B testing in the last 2-3 years are aware of the issue, and Optimizely's software improved."
I am not sure where the "most" comes from in this sentence and what data supports this claim. Many businesses I encounter (and I encounter a wide variety of businesses as a business school professor) are not aware of this issue, or are aware but not sure of the details, or are not sure how to resolve it, etc.
Optimizely's software has indeed improved, but was the problem resolved? Well, it depends on what problem we're talking about.
For this, I recommend that you read the paper and not just the abstract, but this is the gist of it: Optimizely's software in 2014 notified users they can stop an A/B test when the confidence metric was above 95% or below 5%.
However, and this is a big HOWEVER, this is not where we found experimenters stopped their experiments. Many of them stopped when they reached 90% confidence. So the problem that people might have tried to solve since 2014 might not have been the relevant problem to solve.
When I invest time and money in solving a problem, I prefer to know that the problem exists, and what the negative consequences of the problem are. This is what our paper tries to achieve. We simply ask: "Was there a problem, and if there was, how prevalent was it and what consequences did it have".
We then just report our findings (and how we found them). It would also have been interesting if we found no p-hacking and low FDRs, and it was also interesting when we found there was p-hacking and that it did inflate the FDR. Both findings are interesting.
5. "Sensationalized" - Kohavi uses words such as "widespread", "sensationalized", "massive" and "bias" in his text. By using "bias" he actually hints that our results are biased (although what he actually says is that the results are probably correct, just not generalizable).
The paper's abstract is pretty straightforward. It describes the findings of the paper (like any other abstract), and does not use any superlatives. We use very measured language in the paper, and our conclusions very carefully explain what we believe are limitations to conclusions from the data. We have never made any attempt to "sensationalize" the paper. We presented it at conferences, submitted it to peer review, did a standard Wharton podcast about it, etc. Nothing fancy or unique.
So on this point, I don't have a good answer. I'm not sure why Kohavi thinks our paper is sensational. Maybe he means "sensationally interesting"? 🙂
To summarize, what do we actually do in the paper? We test a tradeoff.
On one hand, experimenters should want to avoid false discoveries. It will lower their profits. They should notice it pretty quickly if they run many experiments. On the other hand, the platform was designed in a way that allows naive experimenters to make mistakes.
We went about seeing which one prevails. It turns out that mistakes prevailed (sometimes) but not where expected, and not in the form assumed.
Isn't this an interesting finding?


I am looking for the answer too? Can anyone help?
Universal Flash Storage (UFS) Application development
I would like to write an application to test memory transfers for my UFS device using WINAPI UFS host controller.
Are there any sample application or guidelines provided by Microsoft on how to implement the drivers/ applications for UFS ?


Reference architecture for deploying Nginx on ECS, both as a basic static resource server, and as a reverse proxy in front of a dynamic application server.
NGINX is a high performance HTTP server and reverse proxy which has achieved significant adoption because of its asynchronous event driven architecture which allows it to serve thousands of concurrent requests with very low memory footprint.
Amazon EC2 Container Service (ECS) is a highly scalable, high performance container management service that supports Docker containers and allows you to easily run applications on a managed cluster of Amazon EC2 instances.
This reference architecture shows how to run NGINX containers on a fleet of instances using ECS. You can deploy Nginx as a basic static HTTP file server. Or you can deploy an Nginx reverse proxy container in front of an application container.


- Deploy kaleo-web
- Set Single approver workflow for users
- Deploy Social Office
- Create a SO site
- Invite a member by e-mail
- User gets the invitation by e-mail
- User follows the registration link and creates an account
- Admin approves the registration and adds the user to SO
- User receives a mail about the registration
- User logs in
User doesn't get a notification about the invitation to the site.
User gets a notification about the invitation to the site.


cclite is a lightweight implementation of a community currency registry for local exchange trading systems (LETS) and other alternative money systems. It uses multi-registry, multi-currency, Web-services-based (SOAP) transactions, and is templated to give multi-lingual properties. As of 2009, it has an SMS interface and versions for Debian, cpanel-based, Fedora and Windows XP. As of 2012 it has decimal currencies, templates for many languages, and a REST-style interface for integration with CMS and social networking software.
The RepairsLab system allows you to manage the entry and exit of equipment repair in a small workshop for repairs or service center. The application allows the printing of a "Repairs sheet" for each device and manages the state of repair. Inside the Repairs ticket, you can enter the features of the delivered and the state of repair. The system provides management of different master data for definition of states of repair, customers, types of equipment, trademarks, and models. You can print a paper "Repairs sheet" in duplicate to store paper and as a delivery receipt for the customer.


Adam Devine is the CMO of WorkFusion, architects of AI-powered products that automate customer service functions as well as other business processes. According to Devine, “Adding natural language processes and machine learning changes everything, giving virtual customer assistants (VCAs) the ability to determine not just what rules-based action to take based on a word, but to understand the meaning of words in different combinations, ask questions to create context and intent, and actually do something for the customer.”
chatterpal coupon code
Its Alive helps you make chatbots for tour Facebook page. It enables you to engage with your Facebook community 24×7 with automated content to improve your response time significantly. Chatbots can be used to drive conversations. It can handle frequently asked questions directly through the chatbots by detecting keywords in Facebook Messenger. And it helps you broadcast your messages.
Are you tired of using a traditional vacuum? Or tired of constantly sweeping and dusting to keep your floors clean and clear of dust, dirt, hair, and dander? It’s time to throw out the old ways and embrace the new. And the future is… robot vacuums! In this review of the CleanBot Robot Cleaner, we’ll share how it can help your lifestyle to become more carefree, and – more importantly – cleaner! Use it at home or in the office. There are so many ways it can make things easier. For instance, if you have pets, it’s a great way to stay on top of the dander. Especially if you or a loved one suffers from animal allergens. The automated CleanBot will transform the way you do your chores and keep things clean. If you already know you want the CleanBot Vacuum for your robot vacuum needs, tap any button here now!
Chatfuel helps you make a Facebook chatbot without coding. You can use the editing tools to design chatbots. And you can set up conversational rules in the dashboard. The bot will use NLP to understand and recognize phrases, and will send users predefined answers. You can build a chatbot to serve as a customer service advisor, an event assistant, or a personal avatar. Chatfuel’s analytics technology provides insights about the performance of the bot.
Pros: Building a bot is really simple with their pre-made templates and a drag and drop flow diagram. We have been using Activechat to automate skills aka flows for different visitors (leads and vendors) and touchpoints through different growth tools (referral path to trigger skills). Broadcasting to 'tagged' subscribers is also straightforward. A real game changer.0
Chatterbot Platform or Chatbot Software are computer program which are designed to simulate an intelligent conversation with one or more human users via auditory or textual methods, for engaging in conversation. Chatterbot are text based conversation agent which can interact with human users through some medium, such as an instant message service. The primary aim of such simulation has been to fool the user into thinking that the program's output has been produced by a human. Programs doing this are referred to as Artificial Conversational Entities, talk bots, chatterboxes, chatter robot, chatterbot, chatbot, or chat bot. Some of the chatterbots use natural language processing systems, and some others scan for keywords within the input and respond with a reply with the most matching keywords, or similar wording pattern, from a textual database. Chatterbots are often integrated into dialog systems for various practical applications such as offline help, personalised service, or information acquisition.


When I started using Mozilla, I thought the sidebar's concept was very cool. I thought it would be even better if I could add RSS Sources to it. After some searching, I found DJ Adams mysidebar.pl. While DJ's work seemed like a fine start, it didn't work out that well for me. So I decided to take his lead and create my own incarnation of his work. The main component is sidebar.pl.
If you just want to cut to the chase and add a RSS Source to your Mozilla Sidebar, you can go to my Mozilla Sidebar Installer.
To truely utilize any of the code linked on this page you should be using Mozilla, the Netscape 6 Tech Preview, or the Neoplanet Netscape 6 Tech Preview. You can use the scripts hosted on my server by just installing a new sidebar with the Mozilla Sidebar Installer. Or you can download the scripts below and install them on your own server.
DJ has done some addition work on Mozilla RSS Sidebars recently (8/29/02). You can read about it here. One aspect of what he has done involves standardising a method of specifying the location of the RSS feed for a particular source. This is done through <link> tags, in the format specified here. DJ's new work is an excellent alternative/addition to what is available here.
sidebar.pl is a RSS to HTML translator tuned for Mozilla sidebars. It could be used by itself to display RSS sources on a webpage, but there are better tools for that (see my RSS page).
sidebar-inst.pl installs RSS Sources provided by the sidebar.pl script into Mozilla.


We develop robust technology solutions tailored to your business needs. If you need technology consultation, we will work with you to improve your processes and empower you to run your business without having to worry about technology.
We also specialize in creating software solutions built upon open source technology using agile methods. Creating functional software is much more than writing code. We can help you in any part of the process: requirements & design, construction, testing, and maintenance.
Rikuni means "I have seen" in Quechua, the language of the Incas. We believe software should be built using the Inca's core values of strength in design and beauty.


You are given two strings $$$s$$$ and $$$t$$$ consisting of lowercase Latin letters. Also you have a string $$$z$$$ which is initially empty. You want string $$$z$$$ to be equal to string $$$t$$$. You can perform the following operation to achieve this: append any subsequence of $$$s$$$ at the end of string $$$z$$$. A subsequence is a sequence that can be derived from the given sequence by deleting zero or more elements without changing the order of the remaining elements. For example, if $$$z = ac$$$, $$$s = abcde$$$, you may turn $$$z$$$ into following strings in one operation:

1. $$$z = acace$$$ (if we choose subsequence $$$ace$$$);
2. $$$z = acbcd$$$ (if we choose subsequence $$$bcd$$$);
3. $$$z = acbce$$$ (if we choose subsequence $$$bce$$$).

Note that after this operation string $$$s$$$ doesn't change.

Calculate the minimum number of such operations to turn string $$$z$$$ into string $$$t$$$.


There are n cities in the country where the Old Peykan lives. These cities are located on a straight line, we'll denote them from left to right as c1, c2, ..., cn. The Old Peykan wants to travel from city c1 to cn using roads. There are (n - 1) one way roads, the i-th road goes from city ci to city ci + 1 and is di kilometers long.

The Old Peykan travels 1 kilometer in 1 hour and consumes 1 liter of fuel during this time.

Each city ci (except for the last city cn) has a supply of si liters of fuel which immediately transfers to the Old Peykan if it passes the city or stays in it. This supply refreshes instantly k hours after it transfers. The Old Peykan can stay in a city for a while and fill its fuel tank many times.

Initially (at time zero) the Old Peykan is at city c1 and s1 liters of fuel is transferred to it's empty tank from c1's supply. The Old Peykan's fuel tank capacity is unlimited. Old Peykan can not continue its travel if its tank is emptied strictly between two cities.

Find the minimum time the Old Peykan needs to reach city cn.


03 December 2015Tweet Follow @RezoundTraining
For a limited time, take any Microsoft Certified Professional (MCP) exam, and get a free retake if you don’t pass!
The Second Shot requires you to have scheduled an exam to take between between July 12, 2015, and January 12, 2016. If you don't pass then you need to schedule an appointment for your free retake within the next 30 calendar days of the initial appointment.
Disclaimer - we're providing this information purely to illustrate the main rules and can take no responsibility for your exam bookings made directly via Pearson / Microsoft.
Here's an example:
We've added more details on 'Second Shot' further down this blog page.
Microsoft exams have a number of different questions types which are designed to test your understanding of the particular subject area. MeasureUp practise tests are designed to give you feedback on each of the exam objectives. Simply using practise tests isn't sufficient to master a subject area - you'll need a comprehensive approach covering the breadth and depth of the exam.
In our next blog post we'll expand on the ideas below and provide links for further free and discounted resources. For example, we're currently providing:
To help you with this we're providing comprehensive structured checklists so you know the breadth and depth of the exam's objectives.
For additional information about Second Shot and other offers, email us.


Andrew often reads articles in his favorite magazine 2Char. The main feature of these articles is that each of them uses at most two distinct letters. Andrew decided to send an article to the magazine, but as he hasn't written any article, he just decided to take a random one from magazine 26Char. However, before sending it to the magazine 2Char, he needs to adapt the text to the format of the journal. To do so, he removes some words from the chosen article, in such a way that the remaining text can be written using no more than two distinct letters.

Since the payment depends from the number of non-space characters in the article, Andrew wants to keep the words with the maximum total length.


superscript — A superscript (as in x2, the mathematical notation for x multiplied by itself)
phrase db._phrase |
| | | | )*
superscript identifies text that is to be
displayed as a superscript when rendered.
Formatted inline. Superscripts are usually printed in a smaller font and shifted up with respect to the baseline.
<article xmlns='http://docbook.org/ns/docbook'> <title>Example superscript</title> <para>The equation e<superscript>πi</superscript> + 1 = 0 ties together five of the most important mathematical constants. </para> </article>


Location Based Customer Engagement
Smartphone penetration has risen to over 56% of the Canadian population. Smartphone owners are becoming increasingly reliant on their devices with 66% accessing the Internet every day on their smartphone and most never leaving home without it . Harnessing this trend has never been more important for organizations seeking to capture and retain customers.
Proximity and geo-based marketing engagement allows organizations to provide the right information at the right time and the right place. This allows organizations to drive more revenue, improve customer experience, and develop deeper insights into visitors.
Purple Forge Mobile Apps as a Service (MAaaS) supports comprehensive proximity-based and geo-based user engagement capabilities for iPhone; Android devices currently support a subset of features due to platform limitations.
During the installation of a Purple Forge MAaaS-based application, users may give the application permission to access to their location (e.g. GPS location). In such cases the application can use this information to offer location-enhanced services for information discovery and alerting purposes.
Purple Forge MAaaS information directories can be automatically sorted to show entries that are closest to the user based on their current location. The user can then view this information on a map and getdirections. This allows users to find retail or venue locations that are nearest to them.
Purple Forge MAaaS supports geo-based (also known as geo-fenced) contextual alerts. One or more geographic regions may be specified, as a latitude / longitude and a radius. This means that Purple Forge MAaaS can be configured to take actions to provide information or collect anonymous business intelligence related to specific regions.
Actions can be configured that are associated with specified geographic regions. The actions can be triggered based on whether the user enters or exits specific regions. Available actions include: count entries and exits (anonymous counts), generate an alert message on the user’s phone, generate an alert message with a suggested website to visit (e.g. to offer the user an exit survey), generate an alert and show the user a specific directory information entry (e.g. to tell the user about events, venue specific updates (e.g. renovation updates), tell the user about special offers).
Proximity-based engagement is similar to that of geo-based engagement but is focused around initiating actions based on how close a person is to a location or thing that has a beacon physically associated with it.
Beacons are small physical devices (as small as a quarter), which broadcast a radio signal that can be detected by smartphones. Note that “beacon” and “iBeacon” terms are often used interchangeably. iBeacon is an Apple specific implementation of a beacon.
Proximity to a beacon is viewed as “far”, “near”, or “close”.
Proximity-based use cases include:
- Window advertising, e.g. pop-up offers or incentives to draw someone who is standing in front of a window display to come inside the store and make a purchase
- Advertising, e.g. pop-up information which augments bill board signage
- Offers and deals, e.g. present special offers to users based on their proximity to a specific product or entrance
- Visitor communications, e.g. letting people know about venue changes due to renovations, or alerting visitors in the food-court about closing times and encouraging them to visit retailers prior to departing
- Display a menu, e.g. a prospective customer who is standing in front of the entrance to a restaurant
- Capture analytic information on users around a beacon, e.g. for a window display beacon it will be possible to count the number of users who pass in front of the window display
- Offer on entry to a venue, e.g. contest entry, sign-up for newsletter, on-site events, special offers and deals


On Sat, 21 Aug 2021 at 08:44, Leon Fauster via CentOS-devel <centos-devel at centos.org> wrote: > > On 20.08.21 19:36, Carl George wrote: > > Not exactly. Sometimes RHEL maintainers add digits after %dist, which > > results in NVRs like foo-1.0-1.module_el8.4.0+123+a0a0a0a0.1. It's > > not impossible to parse, but it's much more complicated that just > > ignoring everything after ".module". > > > and BTW > > RH uses the plus sign after module and before %dist : > > RH:qemu-img-4.2.0-48.module+el8.4.0+11909+3300d70f.3.x86_64 > C8:qemu-img-4.2.0-48.module_el8.4.0+885+5e18b468.3.x86_64 > > is this intentional? As far as I know it is intentional to make it clearer to parsing tools that there was a difference. If they used a _ that would break some existing tools and if they used . (dot) it would break a lot more. Using the + was found to break the least of the ones that were being looked at in Fedora 27? days when this was being set up. [Basically various versions were tried, various tools were tested and all kinds of fun and 'laughter' when you find that your Nagios, corporate antivirus, etc now assumes your architecture is .885 versus .x86_64 or some other item. -- Stephen J Smoogen. I've seen things you people wouldn't believe. Flame wars in sci.astro.orion. I have seen SPAM filters overload because of Godwin's Law. All those moments will be lost in time... like posts on BBS... time to reboot.


January 30, 2014
We are excited to announce that we are accepting nominations for Work-Bench’s inaugural Enterprise 30 Under 30 list to recognize the hardworking individuals revolutionizing enterprise technology as we know it.
In an era of unprecedented technological innovation in the enterprise market - marked by the large-scale adoption of cloud-computing, the consumerization of IT, and new paradigms of distribution - never before have enterprise technology startups and their founders been so uniquely positioned to disrupt legacy vendors and redefine how the world’s largest companies work.
Founded on the mission to provide a home to these intrepid entrepreneurs shaping the future of work, Work-Bench would like to recognize those outstanding individuals accelerating this paradigm shift in enterprise IT to more useful, powerful, and better designed business tools.
Whether you run an enterprise technology startup or are innovating within an IT organization of a large corporation, we would love to hear from you. From data scientists to growth hackers, from developers to marketers, we want to know who is shaping the future of work.


Given a lowercase Latin character (letter), check if it appears in the string $$$\texttt{codeforces}$$$.


By doing so, you avoid the hassle of recruitment, HR, and legal obligations. You just hire an engineer to do a certain job on time and pay them only when the job is done well. At the same time, Indeed provides a much more detailed picture, with best math software a backend developer salary reaching up to $119,583 annually. For example, as Twitter, Spotify, Salesforce, and other companies decide to go full-flexible and cut on office spendings, they can pay the money saved to their now-remote workers.
Be prepared to offer a competitive salary, and make sure youscreen your candidatesproperly. Sadly, there are no other sources that compare average salaries for Node.js developers in different states. Clearly, the data provided by different platforms differ from each other.
What can I build with node js?
- Real-Time Chat Application. Node.
- Social Media Platform. Another usage for node js is a social media platform.
- IoT (Internet of Things)
- Streaming App.
- Online Payment Processor.
- Remote Collaboration Tool.
- CRM Tool.
- Single-Page Application.
In this blog, we will explore the Node.JS developer salaries in the USA. In addition, we will check out what Node.JS talents earn in the rest of the world. Collaborate with front-end developers to build an integrated application.
According to Glassdoor, a Node.JS software engineer earns $103,551 per year as an average salary. Considering Latin America, the average salary of NodeJS developers in Colombia is $56K, in Argentina – $60K, in Mexico – $68K per year. But remember that developers’ salaries show the cost that developers earn for their job. Well, according to PayScale, an average base full stack software developer salary is $78,967.
Although I had read a few Turing.com reviews about the onboarding process here, I never really expected it to be this awesome. My coordinator helped me in every single step- from getting hired by top Silicon Valley clients to making sure my needs were met at all levels. Companies should consider hiring a Node.js Developer when they need to develop fast and scalable network applications, data streaming applications, or any manner of collaborative tools.
What does a NodeJS Developer do?
Is node js a compiler?
In this way, Node.js development is very much like a more specialized form of full-stack development. However, businesses can expect to pay lower salaries in regions like Eastern Europe. You may be able to hire a Node.JS developer for $67K per year or more. In comparison, the cost of living is lower in regions like Eastern Europe.
They have a clear understanding of development using NodeJS and are often be mentored by their seniors for learning the ins and outs of programming in the real world. Junior developers often don’t hycm review have the knowledge of a lot of other programming languages apart from their area of expertise. They are helpful in basic coding tasks that do not require creating complicated solutions.
Remote Node js developer jobs with U.S. companies in 2022
The United States is by far not the only location where Node.js engineers reside, so you might want to search abroad. Firstly, I will give an overview of some European countries based on PayScale data and will add information related to Israel for comparison. I will now aggregate and reiterate the previously mentioned data to help you understand the reasons behind higher or lower costs to hire a Node.js developer team in various regions. We will cover Node js developer responsibilities later on, and you will see they are not much different from any other full stack developer with JS expertise.
As a result, many Node.js developers have had some experience with otherlanguagesbefore. Curious, what is the average annual income for Node.js developers in the country? According to Payscale, it is around $59k (€48k) per year, while the hourly rate is $20 (€16). The average monthly salary for Node.js developers in Denmark, according to Salaryexplorer, is about $6,7 . At the same time, the salary for IT specialists with Node.js skills in Copenhagen is $7 . When trying to find out what Node.js developers’ salaries are in Europe, we should not forget that salaries vary for each European country.
Hiring developers from big US cities such as New York, Los Angeles, and San Francisco will cost you much more since the cost of living in these cities is very high. The annual salary in these regions is around $120,000, according to ZipRecruiter, and almost twice more for senior NodeJS engineers. It is the combination of our core business model and values that makes us different from others. We provide full-time, long-term projects to remote developers whereas most of our competitors offer more freelance jobs. We, at Turing, hire remote developers for over 100 skills like React/Node, Python, Angular, Swift, React Native, Android, Java, Rails, Golang, PHP, Vue, among several others.
Node.js is a well-developed resource with an active community around it. That’s hardly surprising, given that there are more and more developers who have started using it for their own purposes. As demand grows, though, it will get more difficult to hire top Node.js developers.
In addition, most developers are “fluent” in several languages, which will affect the final figures. Average salaries are single data points and only one part of a bigger story. It’s expected that many respondents may earn significantly more or significantly less than these averages. However, we hope to map underlying trends by mapping the average salaries for different experience levels. Location is usually one of the biggest factors in how much compensation many developers are paid across the US. In such a vast country, salaries can vary widely from place to place.
Prominent Skills Affecting Software Developer Salary
However, in our experience, the actual costs can be up to 20% higher . Add up the various benefits and be prepared to pay your Node.js engineer up to $119,624 a year. The average full stack developer salary rose by 3% in New York and by 5% in the SF Bay Area. Below the main factors shaping the average Node.js developer salary across the United States are discussed. As you probably know, Node.js developers are responsible for server-side development.
Once you clear these tests, you are eligible to apply to a wide range of jobs available based on your skills. Worldwide, tech companies are actively looking for experienced Node.js developers and practitioners. If you’re a Node.js developer looking for a remote job, you can apply at Turing.com.
Instating a rewarding mentorship program can help form a productive long-term partnership within the team. Most importantly, a junior-level developer must aspire to learn the language better. To ensure they don’t make costly mistakes while doing so, it is a good practice to assign a more experienced mentor to them—a middle-level Node.js developer.
Learn more about our current job openings and benefits of working at FSL. The whole HR process is very fast and smooth – just leave your requirements on the website and you will be immediately contacted. After agreeing on the type of your future team Mobilunity will start looking for candidates. Shortly after you will be given a list of potential hires to choose from.
To hire Nodejs developer might, in fact, be one of the smartest decisions for any business. A Node.js development company could assist you with plenty of things, which typically differ, depending on the company’s needs. Another task such a specialist could assist you with is back-end development, front-end integrations, and such. NodeJS is used by software engineers to write server-side code in addition to the client-side code, and they don’t need to learn a completely new language for it.


- Worldbuilding 13.3k 13.3k 11 gold badge2424 silver badges4646 bronze badges
- Personal Finance & Money 7k 7k 11 gold badge2020 silver badges2828 bronze badges
- Politics 6.6k 6.6k 11 gold badge1313 silver badges2525 bronze badges
- Biology 3k 3k 1515 silver badges1313 bronze badges
- History 1.7k 1.7k 11 gold badge1111 silver badges1111 bronze badges
- View network profile →
Top network posts
- 87 My parents claim they cannot pay for my college education; what are my options?
- 81 How come large herbivores have such thin legs?
- 79 how do you value what your leisure time is worth?
- 78 Why don't US presidential candidates get thrown into jail by their opponent during election?
- 74 Was the "natural born citizen" requirement for the President inserted into the US Constitution by Alexander Hamilton's enemies?
- 65 Wasp hives in a walking corpse. Why would wasps do this?
- 58 Why do people save so much?
- View more network posts →


A patient has been infected with an unknown disease. His body can be seen as an infinite grid of triangular cells which looks as follows:

Two cells are neighboring if they share a side. Therefore, each cell ($$$x$$$, $$$y$$$) has exactly three neighbors:

- ($$$x+1$$$, $$$y$$$)
- ($$$x-1$$$, $$$y$$$)
- ($$$x+1$$$, $$$y-1$$$) if $$$x$$$ is even and ($$$x-1$$$, $$$y+1$$$) otherwise.

Initially some cells are infected, all the others are healthy. The process of recovery begins. Each second, for exactly one cell (even though there might be multiple cells that could change its state) one of the following happens:

- A healthy cell with at least $$$2$$$ infected neighbors also becomes infected.
- An infected cell with at least $$$2$$$ healthy neighbors also becomes healthy.

If no such cell exists, the process of recovery stops. Patient is considered recovered if the process of recovery has stopped and all the cells are healthy.

We're interested in a worst-case scenario: is it possible that the patient never recovers, or if it's not possible, what is the maximum possible duration of the recovery process?


Agora Twitter Integration
- pull agora social media integration
- Twitter could integrate with the Agora, or the other way around, in several seemingly productive ways.
- One way would be to automatically pull tweets that use wikilinks as backlinks in the matching node.
Another way would be to automatically tweet blocks with action tweet for users which added twitter integration, like so:
- tweet if you write a block like this, the Agora will tweet it (exactly once, etc.)
- Probably should support scheduling, throttling, etc.
- The agora will also auto-embed tweets by default. The following is a demo.
🌉 bridge to [[agora social media integration]] built by an Agora user
🏛 Stoas for [[@flancian/agora twitter integration]]
📖 Open document (Hedgedoc) at https://doc.anagora.org/agora-twitter-integration 📖 Open document (Etherpad) at https://stoa.anagora.org/p/agora-twitter-integration