高通 sensor 從native到HAL

来源:https://www.cnblogs.com/linhaostudy/archive/2018/08/13/9470407.html
-Advertisement-
Play Games

app註冊感測器監聽 Android Sensor Framework 的整體架構如下圖所示: 前幾篇sensor相關的文章介紹了sensor的hal的知識,以press_sensor實時顯示氣壓坐標來分析,app層數據獲取的過程,其實實現數據監控非常簡單,主要分為下麵三個步驟: 獲取Sensor服 ...


app註冊感測器監聽

Android Sensor Framework 的整體架構如下圖所示:
image

前幾篇sensor相關的文章介紹了sensor的hal的知識,以press_sensor實時顯示氣壓坐標來分析,app層數據獲取的過程,其實實現數據監控非常簡單,主要分為下麵三個步驟:

  • 獲取Sensor服務:getSystemService;
  • 獲取具體Sensor對象:getDefaultSensor;
  • 註冊數據監聽器:registerListener;

SensorService啟動

開機後,system server啟動時,就會初始化sensor service,也就是說,開機後她一直都在後臺運行著,客戶端部分,直接connect就行了。至於怎麼connect,這一切都被封裝到SensorManager里了。

SensorService服務啟動後,在隨後的第一次被強引用時,其onFirstRef會被調用,緊接著,它會獲取我們的SensorDevice實例:

void SensorService::onFirstRef() {
    ALOGD("nuSensorService starting...");
    SensorDevice& dev(SensorDevice::getInstance());

    sHmacGlobalKeyIsValid = initializeHmacKey();

    if (dev.initCheck() == NO_ERROR) {
        sensor_t const* list;
        ssize_t count = dev.getSensorList(&list);
        if (count > 0) {

附上這部分的流程

SensorDevice作為Sensor架構中native的最後一個文件,與Hal層進行通信,故而在SensorDevice的構造方法中,我們就可以看到著名的hw_get_module和sensors_open_1方法了:

SensorDevice::SensorDevice()
    :  mSensorDevice(0),
       mSensorModule(0) {
    status_t err = hw_get_module(SENSORS_HARDWARE_MODULE_ID,
            (hw_module_t const**)&mSensorModule);

    ALOGE_IF(err, "couldn't load %s module (%s)",
            SENSORS_HARDWARE_MODULE_ID, strerror(-err));

    if (mSensorModule) {
        err = sensors_open_1(&mSensorModule->common, &mSensorDevice);

        ALOGE_IF(err, "couldn't open device for module %s (%s)",
                SENSORS_HARDWARE_MODULE_ID, strerror(-err));

        if (mSensorDevice) {
            if (mSensorDevice->common.version == SENSORS_DEVICE_API_VERSION_1_1 ||
                mSensorDevice->common.version == SENSORS_DEVICE_API_VERSION_1_2) {
                ALOGE(">>>> WARNING <<< Upgrade sensor HAL to version 1_3");
            }

            sensor_t const* list;
            ssize_t count = mSensorModule->get_sensors_list(mSensorModule, &list);
            mActivationCount.setCapacity(count);
            Info model;
            for (size_t i=0 ; i<size_t(count) ; i++) {
                mActivationCount.add(list[i].handle, model);
                mSensorDevice->activate(
                        reinterpret_cast<struct sensors_poll_device_t *>(mSensorDevice),
                        list[i].handle, 0);
            }
        }
    }
}

其中SENSORS_HARDWARE_MODULE_ID是在hardware/sensors.h中定義的module名字:

/**
 * The id of this module
 */
#define SENSORS_HARDWARE_MODULE_ID "sensors"

而mSensorModule就是我們的sensors_module_t結構體,這些都是在hal層sensors.h中定義的:

/**
 * Every hardware module must have a data structure named HAL_MODULE_INFO_SYM
 * and the fields of this data structure must begin with hw_module_t
 * followed by module specific information.
 */
struct sensors_module_t {
    struct hw_module_t common;

    /**
     * Enumerate all available sensors. The list is returned in "list".
     * @return number of sensors in the list
     */
    int (*get_sensors_list)(struct sensors_module_t* module,
            struct sensor_t const** list);

    /**
     *  Place the module in a specific mode. The following modes are defined
     *
     *  0 - Normal operation. Default state of the module.
     *  1 - Loopback mode. Data is injected for the supported
     *      sensors by the sensor service in this mode.
     * @return 0 on success
     *         -EINVAL if requested mode is not supported
     *         -EPERM if operation is not allowed
     */
    int (*set_operation_mode)(unsigned int mode);
};

可以看到sensors_module_t結構體擴展了hw_module_t,它裡面額外提供了get_sensor_list方法來獲取系統支持的sensor列表以及一個模式設置方法。

接下來,我們跟進hw_get_module方法,看看它到底做了什麼?

hw_get_module

該函數具體實現在hardware/libhardware/hardware.c中

int hw_get_module(const char *id, const struct hw_module_t **module)
{
    return hw_get_module_by_class(id, NULL, module);
}
int hw_get_module_by_class(const char *class_id, const char *inst,
                           const struct hw_module_t **module)
{
    int i = 0;
    char prop[PATH_MAX] = {0};
    char path[PATH_MAX] = {0};
    char name[PATH_MAX] = {0};
    char prop_name[PATH_MAX] = {0};
 
 
    if (inst)
        snprintf(name, PATH_MAX, "%s.%s", class_id, inst);
    else
        strlcpy(name, class_id, PATH_MAX);
 
    /*
     * Here we rely on the fact that calling dlopen multiple times on
     * the same .so will simply increment a refcount (and not load
     * a new copy of the library).
     * We also assume that dlopen() is thread-safe.
     */
 
    /* First try a property specific to the class and possibly instance */
    snprintf(prop_name, sizeof(prop_name), "ro.hardware.%s", name);
    if (property_get(prop_name, prop, NULL) > 0) {
        if (hw_module_exists(path, sizeof(path), name, prop) == 0) {
            goto found;
        }
    }
 
    /* Loop through the configuration variants looking for a module */
    for (i=0 ; i<HAL_VARIANT_KEYS_COUNT; i++) {
        if (property_get(variant_keys[i], prop, NULL) == 0) {
            continue;
        }
        if (hw_module_exists(path, sizeof(path), name, prop) == 0) {
            goto found;
        }
    }
 
    /* Nothing found, try the default */
    if (hw_module_exists(path, sizeof(path), name, "default") == 0) {
        goto found;
    }
 
    return -ENOENT;
 
found:
    /* load the module, if this fails, we're doomed, and we should not try
     * to load a different variant. */
    return load(class_id, path, module);
}

我們主要看hw_get_module_by_class,這裡傳入的參數分別是“sensors”,null,以及我們的mSensorModule結構體。

首先將字元串拷貝給name:

strlcpy(name, class_id, PATH_MAX);

接著拼接prop_name為ro.hardware.name,即prop_name=ro.hardware.sensors

通過property_get方法並沒有得到這個值的定義(因為在系統中並沒有對其定義),所以接下來會進入下麵的迴圈:

for (i=0 ; i<HAL_VARIANT_KEYS_COUNT; i++) {
        if (property_get(variant_keys[i], prop, NULL) == 0) {
            continue;
        }
        if (hw_module_exists(path, sizeof(path), name, prop) == 0) {
            goto found;
        }
    }
/**
 * There are a set of variant filename for modules. The form of the filename
 * is "<MODULE_ID>.variant.so" so for the led module the Dream variants 
 * of base "ro.product.board", "ro.board.platform" and "ro.arch" would be:
 *
 * led.trout.so
 * led.msm7k.so
 * led.ARMV6.so
 * led.default.so
 */
 
static const char *variant_keys[] = {
    "ro.hardware",  /* This goes first so that it can pick up a different
                       file on the emulator. */
    "ro.product.board",
    "ro.board.platform",
    "ro.arch"
};

根據上面的解析我門也可以看到,將會分別查找sensors.variant.sosensors.product.sosensors.platform.so,以及sensors.default.so,最終我們會在/system/lib/hw/路徑下找到sensors.msm8909.so,然後將其通過load方法載入進記憶體中運行。由此也可知,我分析的是高通8909平臺。

小細節:當我們實現了自己的HAL層module,並且寫了一個應用程式測試module是否正常工作,那麼在編譯的時候,下麵的參數應該要這樣寫:

LOCAL_MODULE := moduleName.default

或者

LOCAL_MODULE := moduleName.$(TARGET_BOARD_PLATFORM)

由於上面源碼的原因,如果module名字對應不到,你的這個模塊將不會被正常的load進去,因而也就無法正常工作了。

接著我們分析load的實現。

static int load(const char *id,
        const char *path,
        const struct hw_module_t **pHmi)
{
    int status = -EINVAL;
    void *handle = NULL;
    struct hw_module_t *hmi = NULL;
 
    /*
     * load the symbols resolving undefined symbols before
     * dlopen returns. Since RTLD_GLOBAL is not or'd in with
     * RTLD_NOW the external symbols will not be global
     */
    handle = dlopen(path, RTLD_NOW);
    if (handle == NULL) {
        char const *err_str = dlerror();
        ALOGE("load: module=%s\n%s", path, err_str?err_str:"unknown");
        status = -EINVAL;
        goto done;
    }
 
    /* Get the address of the struct hal_module_info. */
    const char *sym = HAL_MODULE_INFO_SYM_AS_STR;
    hmi = (struct hw_module_t *)dlsym(handle, sym);
    if (hmi == NULL) {
        ALOGE("load: couldn't find symbol %s", sym);
        status = -EINVAL;
        goto done;
    }
 
    /* Check that the id matches */
    if (strcmp(id, hmi->id) != 0) {
        ALOGE("load: id=%s != hmi->id=%s", id, hmi->id);
        status = -EINVAL;
        goto done;
    }
 
    hmi->dso = handle;
 
    /* success */
    status = 0;
 
    done:
    if (status != 0) {
        hmi = NULL;
        if (handle != NULL) {
            dlclose(handle);
            handle = NULL;
        }
    } else {
        ALOGV("loaded HAL id=%s path=%s hmi=%p handle=%p",
                id, path, *pHmi, handle);
    }
 
    *pHmi = hmi;
 
    return status;
}
  1. 首先通過dlopen打開sensors.xxx.so模塊,獲得其句柄handle
  2. 調用dlsym去獲取結構體hw_module_t結構體的地址,註意這裡傳入的字元串為HAL_MODULE_INFO_SYM_AS_STR,定義在hardware.h頭文件中
/**
 * Name of the hal_module_info
 */
#define HAL_MODULE_INFO_SYM         HMI
 
/**
 * Name of the hal_module_info as a string
 */
#define HAL_MODULE_INFO_SYM_AS_STR  "HMI"

這裡為什麼要去取名字為HMI的地址,我猜想它應該是HAL模塊的入口了。

ELF文件格式:

ELF = Executable and Linkable Format,可執行連接格式,是UNIX系統實驗室(USL)作為應用程式二進位介面(Application Binary Interface,ABI)而開發和發佈的,擴展名為elf。一個ELF頭在文件的開始,保存了路線圖(road map),描述了該文件的組織情況。sections保存著object 文件的信息,從連接角度看:包括指令,數據,符號表,重定位信息等等。通過file命令我們可知sensors.xx.so是一個ELF文件格式

tiny.hui@build-server:~$ file sensors.msm8909.so
sensors.msm8909.so: ELF 32-bit LSB shared object, ARM, version 1 (SYSV), dynamically linked (uses shared libs), BuildID[md5/uuid]=0x25812b01ab4700281b41f61327075611, not stripped

因此,通過linux的readelf命令我們可以查看該文件的內部佈局及符號表等信息。

tiny.hui@build-server:~$ readelf -s sensors.msm8909.so
 
Symbol table '.dynsym' contains 157 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 00000000     0 FUNC    GLOBAL DEFAULT  UND __cxa_finalize@LIBC (2)
     2: 00000000     0 FUNC    GLOBAL DEFAULT  UND __cxa_atexit@LIBC (2)
     3: 00000000     0 FUNC    GLOBAL DEFAULT  UND __register_atfork@LIBC (2)
     4: 00000000     0 FUNC    GLOBAL DEFAULT  UND pthread_mutex_lock@LIBC (2)
        …………………………// 省略無關信息
    179: 0000c179   120 FUNC    GLOBAL DEFAULT   13 _ZN19NativeSensorManager1
   180: 0000bd21   392 FUNC    GLOBAL DEFAULT   13 _ZN19NativeSensorManager2
   181: 0000a45b   114 FUNC    GLOBAL DEFAULT   13 _ZN24InputEventCircularRe
   182: 000064d9   148 FUNC    GLOBAL DEFAULT   13 _ZN22sensors_poll_context
   183: 0000d889     6 FUNC    GLOBAL DEFAULT   13 _ZN11sensors_XMLC1Ev
   184: 0000663d   156 FUNC    GLOBAL DEFAULT   13 _ZN10SensorBaseC2EPKcS1_P
   185: 000086d5   248 FUNC    GLOBAL DEFAULT   13 _ZN11AccelSensorC1Ev
   186: 000088dd   248 FUNC    GLOBAL DEFAULT   13 _ZN11AccelSensorC2EP13Sen
   187: 00014220     4 OBJECT  GLOBAL DEFAULT   23 _ZN7android9SingletonI11s
   188: 0000a53b    46 FUNC    GLOBAL DEFAULT   13 _ZN18CalibrationManager10
   189: 00007775    56 FUNC    GLOBAL DEFAULT   13 _ZN15ProximitySensorD1Ev
   190: 00014008   136 OBJECT  GLOBAL DEFAULT   22 HMI
   191: 0000721d    26 FUNC    GLOBAL DEFAULT   13 _ZNK11AccelSensor16hasPen
   192: 0000d475    16 FUNC    WEAK   DEFAULT   13 _ZNK7android12SortedVecto
   193: 00006dd9   264 FUNC    GLOBAL DEFAULT   13 _ZN11LightSensorC2EPc
   194: 00006181    48 FUNC    GLOBAL DEFAULT   13 _ZN22sensors_poll_context
   195: 0000d4fd    48 FUNC    GLOBAL DEFAULT   13 _ZN13VirtualSensorD1Ev
   196: 0000aa15    80 FUNC    GLOBAL DEFAULT   13 _ZN18CalibrationManagerD2
   197: 000087cd   272 FUNC    GLOBAL DEFAULT   13 _ZN11AccelSensorC1EPc

由符號表可知,HMI的地址為00014008,拿到函數地址,當然就可以執行對應的代碼了。

QualComm Sensor HAL

因此我們接著看sensor_hal層,高通的Sensor實現了自己的HAL,其源碼在hardware\qcom\sensors路徑下,通過Android.mk我們也可以確定他確實是我們前面load方法打開的動態鏈接庫,其編譯後會生成sensor.msm8909.so

ifneq ($(filter msm8960 msm8610 msm8916 msm8909,$(TARGET_BOARD_PLATFORM)),)
# Exclude SSC targets
ifneq ($(TARGET_USES_SSC),true)
# Disable temporarily for compilling error
ifneq ($(BUILD_TINY_ANDROID),true)
LOCAL_PATH := $(call my-dir)

# HAL module implemenation stored in
include $(CLEAR_VARS)

ifeq ($(USE_SENSOR_MULTI_HAL),true)
  LOCAL_MODULE := sensors.native
else
  ifneq ($(filter msm8610,$(TARGET_BOARD_PLATFORM)),)
    LOCAL_MODULE := sensors.$(TARGET_BOARD_PLATFORM)
    LOCAL_CFLAGS := -DTARGET_8610
  else
    ifneq ($(filter msm8916 msm8909,$(TARGET_BOARD_PLATFORM)),)
      LOCAL_MODULE := sensors.$(TARGET_BOARD_PLATFORM)
    else
      LOCAL_MODULE := sensors.msm8960
    endif
  endif

  ifdef TARGET_2ND_ARCH
    LOCAL_MODULE_RELATIVE_PATH := hw
  else
    LOCAL_MODULE_PATH := $(TARGET_OUT_SHARED_LIBRARIES)/hw
  endif
endif

LOCAL_MODULE_TAGS := optional

LOCAL_CFLAGS += -DLOG_TAG=\"Sensors\"
ifeq ($(call is-board-platform,msm8960),true)
  LOCAL_CFLAGS += -DTARGET_8930
endif

LOCAL_C_INCLUDES := $(TARGET_OUT_INTERMEDIATES)/KERNEL_OBJ/usr/include
LOCAL_ADDITIONAL_DEPENDENCIES := $(TARGET_OUT_INTERMEDIATES)/KERNEL_OBJ/usr

# Export calibration library needed dependency headers
LOCAL_COPY_HEADERS_TO := sensors/inc
LOCAL_COPY_HEADERS :=   \
                CalibrationModule.h \
                sensors_extension.h \
                sensors.h

LOCAL_SRC_FILES :=      \
                sensors.cpp                     \
                SensorBase.cpp                  \
                LightSensor.cpp                 \
                ProximitySensor.cpp             \
                CompassSensor.cpp               \
                Accelerometer.cpp                               \
                Gyroscope.cpp                           \
                Bmp180.cpp                              \
                InputEventReader.cpp \
                CalibrationManager.cpp \
                NativeSensorManager.cpp \
                VirtualSensor.cpp       \
                sensors_XML.cpp \
                SignificantMotion.cpp

LOCAL_C_INCLUDES += external/libxml2/include    \

ifeq ($(call is-platform-sdk-version-at-least,20),true)
    LOCAL_C_INCLUDES += external/icu/icu4c/source/common
else
    LOCAL_C_INCLUDES += external/icu4c/common
endif

LOCAL_SHARED_LIBRARIES := liblog libcutils libdl libxml2 libutils

include $(BUILD_SHARED_LIBRARY)

include $(CLEAR_VARS)

LOCAL_MODULE := libcalmodule_common
LOCAL_SRC_FILES := \
                   algo/common/common_wrapper.c \
                   algo/common/compass/AKFS_AOC.c \
                   algo/common/compass/AKFS_Device.c \
                   algo/common/compass/AKFS_Direction.c \
                   algo/common/compass/AKFS_VNorm.c

LOCAL_SHARED_LIBRARIES := liblog libcutils
LOCAL_MODULE_TAGS := optional

ifdef TARGET_2ND_ARCH
LOCAL_MODULE_PATH_32 := $(TARGET_OUT_VENDOR)/lib
LOCAL_MODULE_PATH_64 := $(TARGET_OUT_VENDOR)/lib64
else
LOCAL_MODULE_PATH := $(TARGET_OUT_VENDOR_SHARED_LIBRARIES)
endif

include $(BUILD_SHARED_LIBRARY)

include $(CLEAR_VARS)

LOCAL_MODULE := calmodule.cfg
LOCAL_MODULE_TAGS := optional
LOCAL_MODULE_CLASS := ETC
LOCAL_MODULE_PATH := $(TARGET_OUT_VENDOR_ETC)
LOCAL_SRC_FILES := calmodule.cfg

include $(BUILD_PREBUILT)

endif #BUILD_TINY_ANDROID
endif #TARGET_USES_SSC
endif #TARGET_BOARD_PLATFORM

那麼HMI的入口到底定義在這裡的那個文件中呢?

功夫不負有心人,在sensors.cpp中,我們終於找到了HMI的入口,即下麵的結構體:

static struct hw_module_methods_t sensors_module_methods = {
    .open = sensors_open
};
 
struct sensors_module_t HAL_MODULE_INFO_SYM = {
    .common = {
        .tag = HARDWARE_MODULE_TAG,
        .module_api_version = (uint16_t)SENSORS_DEVICE_API_VERSION_1_3,
        .hal_api_version = HARDWARE_HAL_API_VERSION,
        .id = SENSORS_HARDWARE_MODULE_ID,
        .name = "QTI Sensors Module",
        .author = "Qualcomm Technologies, Inc.",
        .methods = &sensors_module_methods,
        .dso = NULL,
        .reserved = {0},
    },
    .get_sensors_list = sensors_get_sensors_list,
    .set_operation_mode = sensors_set_operation_mode
};

HAL_MODULE_INFO_SYM即上文提到的HMI變數,恭喜各位,這裡我們就開啟了QualComm Sensor HAL的大門。

最後這個hw_module_t的結構體句柄會返回給我們的SensorDevice的構造函數里:

SensorDevice::SensorDevice()
    :  mSensorDevice(0),
       mSensorModule(0)
{
    status_t err = hw_get_module(SENSORS_HARDWARE_MODULE_ID,
            (hw_module_t const**)&mSensorModule);
 
    ALOGE_IF(err, "couldn't load %s module (%s)",
            SENSORS_HARDWARE_MODULE_ID, strerror(-err));
 
    if (mSensorModule) {
        err = sensors_open_1(&mSensorModule->common, &mSensorDevice);

接著,通過sensors_open_1方法將module->common傳入,打開我們的sensor驅動。

// hardware/libhardware/include/hardware/sensors.h
static inline int sensors_open_1(const struct hw_module_t* module,
        sensors_poll_device_1_t** device) {
    return module->methods->open(module,
            SENSORS_HARDWARE_POLL, (struct hw_device_t**)device);
}
 
static inline int sensors_close_1(sensors_poll_device_1_t* device) {
    return device->common.close(&device->common);
}

回過頭去看看HMI的結構體定義,其中module->common->open被賦值為sensors_module_methods,其只有一個open方法,因此,module->methods->open最終會調用sensors_open方法來打開驅動程式。

到這裡native到hal層的邏輯其實已經基本上分析完了。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • C#中欄位、屬性和構造函數賦值的問題 提出問題 首先提出幾個問題: 1、如何實現自己的註入框架? 2、欄位和自動屬性的區別是什麼? 3、欄位和自動屬性聲明時的直接賦值和構造函數賦值有什麼區別? 4、為什麼只讀欄位和只讀自動屬性(只有get沒有set訪問器)都可以在構造函數中進行賦值? 5、反射可以給 ...
  • 概述 InfiniteCanvas 是一個 Canvas 控制項,它支持無限畫布的滾動,支持 Ink,文本,格式文本,畫布縮放操作,撤銷重做操作,導入和導出數據。 這是一個非常實用的控制項,在“來畫視頻” UWP 應用的繪畫功能中,也用到了這個控制項,它對不同畫筆的選擇,橡皮擦,直尺和圓形尺,文字輸入和字 ...
  • 前言 平時工作中下拉聯動是相對比較麻煩的地方,雖然邏輯簡單,但是需要寫一堆js跟ajax請求。現在打算在.net core mvc下封裝一個下拉聯動組件方便使用。 下麵將實現一個 國家 語言 省市區 的多級聯動 創建實體模型 寫出對應下拉的Action方法 這裡 下拉contorler名稱約定為Dr ...
  • 1.1 腳本認識 1 #!/bin/bash 1 #!/bin/bash 第一行,通常用來指定執行腳本的shell ,/bin/bash是CentOS 預設的shell 如果寫到第二行,就是註釋了 1 #!/bin/sh 1 #!/bin/sh 這樣寫也是可以的,sh是bash的軟鏈接,和#!/bi ...
  • uptime     uptime命令功能比較簡單,主要功能如下所示: 查看伺服器的開機時長 查看CPU負載 基本用法 用法示例 輸出內容解釋如下所示: | 顯示內容 | 解釋 | | | | | 15:33:08 | 系統當前時間 | | up 10 days, 4:40 | ...
  • Yum的介紹 Yum 全稱為 Yellow dog Updater, Modified,它是一個線上的軟體安裝命令。 他能夠從指定的伺服器自動下載RPM包並且安裝,可以自動處理依賴性關係,並且一次安裝所有依賴的軟體包,無須繁瑣地一次次下載、安裝。yum提供了查找、安裝、刪除某一個、一組甚至全部軟體包 ...
  • 最近在使用 docker-ce ,在配置當前用戶組為 docker 的時候(sudo usermod -aG docker $USER)發現:必須要關閉當前的 session 重新登錄 後,才能使得修改的組生效。 通常情況下,因為當前環境還運行很多軟體,如果重新登錄session的話,很不方便。我通 ...
  • 今天終終終於開始寫博客啦,雖然是個小白,但是會一直努力的,還望大家多多支持。以後會不定期的推出各種問題供大家討論,然後有什麼新鮮知識,技巧等一定第一時間和大家分享!!! ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...