---
# System prepended metadata

title: 6/14 Microsoft
tags: [Azure, 微軟]

---

---
tags: 微軟,Azure
---


# 6/14 Microsoft
# Azure IoT 中樞 教學課程 (續)
## 訊息擴充 (message enrichments )
訊息擴充 (message enrichments )功能能在訊息送至指定端點前附加額外資訊，可用來簡化下游處理的資料。
> 教學課程中，會建立指向兩個不同的儲存體容器的兩個端點(enriched、original)。將訊息傳送至 IoT 中樞時，它會傳送至這兩個儲存體容器。只有被傳送至enriched儲存體容器端點的訊息會被擴充。

sample code：https://github.com/Azure-Samples/azure-iot-samples-csharp/archive/master.zip

* 安裝和設定資源
```
# This command retrieves the subscription id of the current Azure account.
# This field is used when setting up the routing rules.
subscriptionID=$(az account show --query id -o tsv)

# Concatenate this number onto the resources that have to be globally unique.
# You can set this to "" or to a specific value if you don't want it to be random.
# This retrieves a random value.
randomValue=$RANDOM

# This command installs the IOT Extension for Azure CLI.
# You only need to install this the first time.
# You need it to create the device identity.
az extension add --name azure-cli-iot-ext

# Set the values for the resource names that
#   don't have to be globally unique.
location=westus2
resourceGroup=ContosoResourcesMsgEn
containerName1=original
containerName2=enriched
iotDeviceName=Contoso-Test-Device

# Create the resource group to be used
#   for all the resources for this tutorial.
az group create --name $resourceGroup \
    --location $location

# The IoT hub name must be globally unique,
#   so add a random value to the end.
iotHubName=ContosoTestHubMsgEn$randomValue
echo "IoT hub name = " $iotHubName

# Create the IoT hub.
az iot hub create --name $iotHubName \
    --resource-group $resourceGroup \
    --sku S1 --location $location

# You need a storage account that will have two containers
#   -- one for the original messages and
#   one for the enriched messages.
# The storage account name must be globally unique,
#   so add a random value to the end.
storageAccountName=contosostorage$randomValue
echo "Storage account name = " $storageAccountName

# Create the storage account to be used as a routing destination.
az storage account create --name $storageAccountName \
    --resource-group $resourceGroup \
    --location $location \
    --sku Standard_LRS

# Get the primary storage account key.
#    You need this to create the containers.
storageAccountKey=$(az storage account keys list \
    --resource-group $resourceGroup \
    --account-name $storageAccountName \
    --query "[0].value" | tr -d '"')

# See the value of the storage account key.
echo "storage account key = " $storageAccountKey

# Create the containers in the storage account.
az storage container create --name $containerName1 \
    --account-name $storageAccountName \
    --account-key $storageAccountKey \
    --public-access off

az storage container create --name $containerName2 \
    --account-name $storageAccountName \
    --account-key $storageAccountKey \
    --public-access off

# Create the IoT device identity to be used for testing.
az iot hub device-identity create --device-id $iotDeviceName \
    --hub-name $iotHubName

# Retrieve the information about the device identity, then copy the primary key to
#   Notepad. You need this to run the device simulation during the testing phase.
# If you are using Cloud Shell, you can scroll the window back up to retrieve this value.
az iot hub device-identity show --device-id $iotDeviceName \
    --hub-name $iotHubName

##### ROUTING FOR STORAGE #####

# You're going to have two routes and two endpoints.
# One points to container1 in the storage account
#   and includes all messages.
# The other points to container2 in the same storage account
#   and only includes enriched messages.

endpointType="azurestoragecontainer"
endpointName1="ContosoStorageEndpointOriginal"
endpointName2="ContosoStorageEndpointEnriched"
routeName1="ContosoStorageRouteOriginal"
routeName2="ContosoStorageRouteEnriched"

# for both endpoints, retrieve the messages going to storage
condition='level="storage"'

# Get the connection string for the storage account.
# Adding the "-o tsv" makes it be returned without the default double quotes around it.
storageConnectionString=$(az storage account show-connection-string \
  --name $storageAccountName --query connectionString -o tsv)

# Create the routing endpoints and routes.
# Set the encoding format to either avro or json.

# This is the endpoint for container 1, for endpoint messages that are not enriched.
az iot hub routing-endpoint create \
  --connection-string $storageConnectionString \
  --endpoint-name $endpointName1 \
  --endpoint-resource-group $resourceGroup \
  --endpoint-subscription-id $subscriptionID \
  --endpoint-type $endpointType \
  --hub-name $iotHubName \
  --container $containerName1 \
  --resource-group $resourceGroup \
  --encoding json

# This is the endpoint for container 2, for endpoint messages that are enriched.
az iot hub routing-endpoint create \
  --connection-string $storageConnectionString \
  --endpoint-name $endpointName2 \
  --endpoint-resource-group $resourceGroup \
  --endpoint-subscription-id $subscriptionID \
  --endpoint-type $endpointType \
  --hub-name $iotHubName \
  --container $containerName2 \
  --resource-group $resourceGroup \
  --encoding json

# This is the route for messages that are not enriched.
# Create the route for the first storage endpoint.
az iot hub route create \
  --name $routeName1 \
  --hub-name $iotHubName \
  --source devicemessages \
  --resource-group $resourceGroup \
  --endpoint-name $endpointName1 \
  --enabled \
  --condition $condition

# This is the route for messages that are not enriched.
az iot hub route create \
  --name $routeName2 \
  --hub-name $iotHubName \
  --source devicemessages \
  --resource-group $resourceGroup \
  --endpoint-name $endpointName2 \
  --enabled \
  --condition $condition
```

* 檢視路由及設定訊息類
資源群組 > IoT 中樞 > 訊息路由 > "Enrich messages - preview"

![](https://i.imgur.com/2UqODbF.png)

* 將訊息傳送至 IoT 中樞
> 中樞已設定規則：
> * 將訊息路由傳送至儲存體端點 ContosoStorageEndpointOriginal 不進行擴充，並會儲存在儲存體容器original。
> * 會擴充並儲存在儲存體容器中將訊息路由傳送至儲存體端點 ContosoStorageEndpointEnriched enriched。

* 將訊息傳送至 IoT 中樞
```
cd azure-iot-samples-csharp-master\iot-hub\Tutorials\Routing\SimulatedDevice
```
修改Program.cs
```
static string myDeviceId = "contoso-test-device";
static string iotHubUri = "ContosoTestHubMsgEn.azure-devices.net";
// This is the primary key for the device. This is in the portal.
// Find your IoT hub in the portal > IoT devices > select your device > copy the key.
static string deviceKey = "{your device key here}";
```
* 執行應用程式
```
dotnet restore
dotnet run
```
* 檢視資料
> 等待數分鐘後

資源群組 > 儲存體帳戶 > 儲存體總管 (預覽) > BLOB CONTAINERS
![](https://i.imgur.com/UEqfmKz.png)
下載結果可發現：
enriched：
```
{"EnqueuedTimeUtc":"2019-06-07T15:32:12.4430000Z","Properties":{"level":"storage","myIotHub":"contosotesthubmsgen6147","Device location":"$twin.tags.location\t","customerID":"6ce345b8-1e4a-411e-9398-d34587459a3a"},"SystemProperties":{"connectionDeviceId":"Contoso-Test-Device","connectionAuthMethod":"{\"scope\":\"device\",\"type\":\"sas\",\"issuer\":\"iothub\",\"acceptingIpFilterRule\":null}","connectionDeviceGenerationId":"636955163414598356","enqueuedTime":"2019-06-07T15:32:12.4430000Z"},"Body":"eyJkZXZpY2VJZCI6IkNvbnRvc28tVGVzdC1EZXZpY2UiLCJ0ZW1wZXJhdHVyZSI6MjUuNjI1NzU4Mzc2MjYzOTAzLCJodW1pZGl0eSI6NjAuMzg5MjU0NjcwNzcxNDIyLCJwb2ludEluZm8iOiJUaGlzIGlzIGEgc3RvcmFnZSBtZXNzYWdlLiJ9"}
```
original：
```
{"EnqueuedTimeUtc":"2019-06-07T15:32:12.4430000Z","Properties":{"level":"storage"},"SystemProperties":{"connectionDeviceId":"Contoso-Test-Device","connectionAuthMethod":"{\"scope\":\"device\",\"type\":\"sas\",\"issuer\":\"iothub\",\"acceptingIpFilterRule\":null}","connectionDeviceGenerationId":"636955163414598356","enqueuedTime":"2019-06-07T15:32:12.4430000Z"},"Body":"eyJkZXZpY2VJZCI6IkNvbnRvc28tVGVzdC1EZXZpY2UiLCJ0ZW1wZXJhdHVyZSI6MjUuNjI1NzU4Mzc2MjYzOTAzLCJodW1pZGl0eSI6NjAuMzg5MjU0NjcwNzcxNDIyLCJwb2ludEluZm8iOiJUaGlzIGlzIGEgc3RvcmFnZSBtZXNzYWdlLiJ9"}
```
比較可發現original版本裡面沒有"My IoT Hub", "device location", and "customerID"等資訊。


## 使用計量和診斷紀錄
> 建議設定某些計量並啟用診斷記錄，之後若發生問題，就可以查看資料來協助您診斷問題，並加快修正速度。
> 本篇目標：啟用診斷記錄(設定某些要監看的計量，以及會在計量達到特定界限時引發的警示)、檢查記錄來找出錯誤
> eg. 讓系統在所傳送的遙測訊息數目超過特定界限時，或是在所使用的訊息數目接近 IoT 中樞每日允許的訊息配額時，傳送電子郵件

* 建立資源
```
# This is the IOT Extension for Azure CLI.
# You only need to install this the first time.
# You need it to create the device identity. 
az extension add --name azure-cli-iot-ext

# Set the values for the resource names that don't have to be globally unique.
# The resources that have to have unique names are named in the script below
#   with a random number concatenated to the name so you can probably just
#   run this script, and it will work with no conflicts.
location=westus
resourceGroup=ContosoResources
iotDeviceName=Contoso-Test-Device 

# Create the resource group to be used
#   for all the resources for this tutorial.
az group create --name $resourceGroup \
    --location $location

# The IoT hub name must be globally unique, so add a random number to the end.
iotHubName=ContosoTestHub$RANDOM
echo "IoT hub name = " $iotHubName

# Create the IoT hub in the Free tier.
az iot hub create --name $iotHubName \
    --resource-group $resourceGroup \
    --sku F1 --location $location

# The storage account name must be globally unique, so add a random number to the end.
storageAccountName=contosostoragemon$RANDOM
echo "Storage account name = " $storageAccountName

# Create the storage account.
az storage account create --name $storageAccountName \
    --resource-group $resourceGroup \
    --location $location \
    --sku Standard_LRS

# Create the IoT device identity to be used for testing.
az iot hub device-identity create --device-id $iotDeviceName \
    --hub-name $iotHubName 

# Retrieve the information about the device identity, then copy the primary key to
#   Notepad. You need this to run the device simulation during the testing phase.
az iot hub device-identity show --device-id $iotDeviceName \
    --hub-name $iotHubName
```
* 啟用診斷記錄
資源群組 (Contoso-Resources) > IoT 中樞 > 診斷設定 > 新增診斷設定
![](https://i.imgur.com/sOCUS9s.png)

![](https://i.imgur.com/B6sBSQ0.png)

* 設定計量
資源群組 (Contoso-Resources) > IoT 中樞 > 計量
![](https://i.imgur.com/vsitO8U.png)
![](https://i.imgur.com/cyg2syJ.png)

* 設定警示
資源群組 (Contoso-Resources) > IoT 中樞 > 警訊 > 檢視傳統警示 > 新增傳統警示 (針對"已傳送的遙測訊息")
![](https://i.imgur.com/zx9DHF7.png)

![](https://i.imgur.com/xg09GXb.png)

![](https://i.imgur.com/hJc0uQV.png)

以同樣方法，針對"已使用的訊息總數"，建立傳統警示
![](https://i.imgur.com/J9Wy8jI.png)

### 執行模擬裝置應用程式
sample code：https://github.com/Azure-Samples/azure-iot-samples-csharp/archive/master.zip

```
cd azure-iot-samples-csharp-master\iot-hub\Tutorials\Routing\SimulatedDevice
```
修改Program.cs
```
static string myDeviceId = "contoso-test-device";
static string iotHubUri = "ContosoTestHub.azure-devices.net";
// This is the primary key for the device. This is in the portal. 
// Find your IoT hub in the portal > IoT devices > select your device > copy the key. 
static string deviceKey = "{your device key here}";
```
> iotHubUri =>  IoT 中樞主機名稱
> deviceKey => 裝置金鑰
> Task.Delay：1000 => 10
* 執行和測試
```
dotnet restore
dotnet run
```
![](https://i.imgur.com/z4GwhMA.png)
![](https://i.imgur.com/1UrtrT1.png)
![](https://i.imgur.com/dma8lYj.png)

* 查看診斷記錄
資源群組 > 儲存體帳戶 > Blob > 容器 insights-logs-connections
![](https://i.imgur.com/vB59AGC.png)

## 執行手動容錯移轉
> 手動容錯移轉可讓客戶將其中樞的作業從主要區域容錯移轉到對應的 Azure 異地配對區域。發生區域性災難或延伸服務中斷的情況時，可以進行手動容錯移轉。

* 建立 IoT 中樞
> 同其他教學

* 執行手動容錯移轉
資源群組 > IoT 中樞 > 手動容錯移轉 (預覽)
![](https://i.imgur.com/aq20Cag.png)

初始容錯移轉 > 填寫 IoT 中樞的名稱 > 確定
> 過程中可看見 "從 West US 手動容錯移轉到 East US 正在進行中..."的狀態

![](https://i.imgur.com/xjLkoGM.png)

容錯轉移完成後，可看見 IoT 中樞位置調換
![](https://i.imgur.com/X3rA9nF.png)


* 執行容錯回復
步驟同"執行手動容錯移轉"
> 如果您剛執行完容錯移轉，必須等候大約一小時，才能要求容錯回復。



## 設定裝置
> 除了從裝置接收遙測資料，可能也需要從後端服務設定您的裝置。

若要同步處理裝置與 IoT 中樞之間的狀態資訊，可使用裝置對應項(device twins，包含desired properties, reported properties, tags)
> desired properties：由後端應用程式所設定，供裝置讀取
> reported properties：由裝置所設定，供後端應用程式讀取
> tags：由後端應用程式所設定，且一律不會傳送至裝置

![](https://i.imgur.com/qK4LoGa.png)

* 下載所需範例程式
https://github.com/Azure-Samples/azure-iot-samples-node/archive/master.zip
* 設定 Azure 資源
> Azure CLI
```
hubname=tutorial-iot-hub
location=centralus

# Install the IoT extension if it's not already installed:
az extension add --name azure-cli-iot-ext

# Create a resource group:
az group create --name tutorial-iot-hub-rg --location $location

# Create your free-tier IoT Hub. You can only have one free IoT Hub per subscription:
az iot hub create --name $hubname --location $location --resource-group tutorial-iot-hub-rg --sku F1

# Make a note of the service connection string, you need it later:
az iot hub show-connection-string --name $hubname -o table

# Set the name of your IoT hub:
hubname=tutorial-iot-hub

# Create the device in the identity registry:
az iot hub device-identity create --device-id MyTwinDevice --hub-name $hubname --resource-group tutorial-iot-hub-rg

# Retrieve the device connection string, you need this later:
az iot hub device-identity show-connection-string --device-id MyTwinDevice --hub-name $hubname --resource-group tutorial-iot-hub-rg -o table
```
* service connection string

```
HostName=tutorial-iot-hub000000000000.azure-devices.net;SharedAccessKeyName=iothubowner;SharedAccessKey=gKDryI8xfYbNP+z+PanGU5vW/PVukoC5XzG4H+DwX98=
```
* device connection string
```
HostName=tutorial-iot-hub000000000000.azure-devices.net;DeviceId=MyTwinDevice;SharedAccessKey=DlqakhZN6T3YVd7xLIpGZhxbcNst8XnR57FIkCcWE5A=
```

### 傳送狀態資訊
```
cd azure-iot-samples-node-master\iot-hub\Tutorials\DeviceTwins
```
> SimulatedDevice.js 程式碼說明
* 使用裝置連接字串連線至 IoT 中樞
```
// Get the device connection string from a command line argument
var connectionString = process.argv[2];
```
* 從用戶端物件中取得對應項
```
// Get the device twin
client.getTwin(function(err, twin) {
  if (err) {
    console.error(chalk.red('Could not get device twin'));
  } else {
    console.log(chalk.green('Device twin created'));
```
* 建立所需屬性更新的處理常式(handlers)，以回應 JSON 階層中不同層級的更新 (檢視從後端應用程式傳送至裝置的所有所需屬性變更)
```
// Handle all desired property updates
twin.on('properties.desired', function(delta) {
    console.log(chalk.yellow('\nNew desired properties received in patch:'));

// Handle changes to the fanOn desired property
twin.on('properties.desired.fanOn', function(fanOn) {
    console.log(chalk.green('\nSetting fan state to ' + fanOn));

    // Update the reported property after processing the desired property
    reportedPropertiesPatch.fanOn = fanOn ? fanOn : '{unknown}';
});
```
* 本機對應項物件會儲存一組完整的desired properties和reported properties。 從後端傳送的差異可能僅更新了desired properties的子集。
```
// Handle desired properties updates to the climate component
twin.on('properties.desired.components.climate', function(delta) {
    if (delta.minTemperature || delta.maxTemperature) {
      console.log(chalk.green('\nUpdating desired tempertures in climate component:'));
      console.log('Configuring minimum temperature: ' + twin.properties.desired.components.climate.minTemperature);
      console.log('Configuring maximum temperture: ' + twin.properties.desired.components.climate.maxTemperature);

      // Update the reported properties and send them to the hub
      reportedPropertiesPatch.minTemperature = twin.properties.desired.components.climate.minTemperature;
      reportedPropertiesPatch.maxTemperature = twin.properties.desired.components.climate.maxTemperature;
      sendReportedProperties();
    }
});
```
* 從後端傳送的所需屬性不會指出正在對特定的所需屬性執行何種作業，程式碼必須從目前儲存於本機的所需屬性集，以及從中樞傳送的變更，來推斷作業類型。
> 模擬裝置如何對所需屬性中所列的元件處理插入、更新和刪除作業
```
// Keep track of all the components the device knows about
var componentList = {};

// Use this componentList list and compare it to the delta to infer
// if anything was added, deleted, or updated.
twin.on('properties.desired.components', function(delta) {
  if (delta === null) {
    componentList = {};
  }
  else {
    Object.keys(delta).forEach(function(key) {

      if (delta[key] === null && componentList[key]) {
        // The delta contains a null value, and the
        // device has a record of this component.
        // Must be a delete operation.
        console.log(chalk.green('\nDeleting component ' + key));
        delete componentList[key];

      } else if (delta[key]) {
        if (componentList[key]) {
          // The delta contains a component, and the
          // device has a record of it.
          // Must be an update operation.
          console.log(chalk.green('\nUpdating component ' + key + ':'));
          console.log(JSON.stringify(delta[key]));
          // Store the complete object instead of just the delta
          componentList[key] = twin.properties.desired.components[key];

        } else {
          // The delta contains a component, and the
          // device has no record of it.
          // Must be an add operation.
          console.log(chalk.green('\nAdding component ' + key + ':'));
          console.log(JSON.stringify(delta[key]));
          // Store the complete object instead of just the delta
          componentList[key] = twin.properties.desired.components[key];
        }
      }
    });
  }
});
```
* 將所需屬性變更從後端應用程式傳送至裝置
```
cd azure-iot-samples-node-master\iot-hub\Tutorials\DeviceTwins
```
> ServiceClient.js 程式碼說明
* 連線至裝置身分識別登錄，並存取特定裝置的對應項
```
// Create a device identity registry object
var registry = Registry.fromConnectionString(connectionString);

// Get the device twin and send desired property update patches at intervals.
// Print the reported properties after some of the desired property updates.
registry.getTwin(deviceId, async (err, twin) => {
  if (err) {
    console.error(err.message);
  } else {
    console.log('Got device twin');
```

* 後端應用程式傳送至裝置的所需屬性修補程式(patches)
```
// Turn the fan on
var twinPatchFanOn = {
  properties: {
    desired: {
      patchId: "Switch fan on",
      fanOn: "false",
    }
  }
};

// Set the maximum temperature for the climate component
var twinPatchSetMaxTemperature = {
  properties: {
    desired: {
      patchId: "Set maximum temperature",
      components: {
        climate: {
          maxTemperature: "92"
        }
      }
    }
  }
};

// Add a new component
var twinPatchAddWifiComponent = {
  properties: {
    desired: {
      patchId: "Add WiFi component",
      components: {
        wifi: { 
          channel: "6",
          ssid: "my_network"
        }
      }
    }
  }
};

// Update the WiFi component
var twinPatchUpdateWifiComponent = {
  properties: {
    desired: {
      patchId: "Update WiFi component",
      components: {
        wifi: { 
          channel: "13",
          ssid: "my_other_network"
        }
      }
    }
  }
};

// Delete the WiFi component
var twinPatchDeleteWifiComponent = {
  properties: {
    desired: {
      patchId: "Delete WiFi component",
      components: {
        wifi: null
      }
    }
  }
};
```
* 後端應用程式將所需屬性更新傳送至裝置
```
// Send a desired property update patch
async function sendDesiredProperties(twin, patch) {
  twin.update(patch, (err, twin) => {
    if (err) {
      console.error(err.message);
    } else {
      console.log(chalk.green(`\nSent ${twin.properties.desired.patchId} patch:`));
      console.log(JSON.stringify(patch, null, 2));
    }
  });
}
```
#### 執行應用程式
* 執行模擬裝置應用程式
```
cd azure-iot-samples-node-master\iot-hub\Tutorials\DeviceTwins
npm install
node SimulatedDevice.js "{your device connection string}"
```
* 執行後端應用程式
```
cd azure-iot-samples-node-master\iot-hub\Tutorials\DeviceTwins
npm install
node ServiceClient.js "{your service connection string}"
```
![](https://i.imgur.com/so9sv1e.png)
![](https://i.imgur.com/vFQtc6o.png)

### 接收狀態資訊
> 後端應用程式會以報告屬性的形式接收來自裝置的狀態資訊，後端應用程式可從儲存於中樞的裝置對應項讀取報告屬性的目前值。

* 模擬裝置所傳送之修補程式 (更新修補程式中的欄位，再將其傳送至中樞)
```
// Create a patch to send to the hub
var reportedPropertiesPatch = {
  firmwareVersion:'1.2.1',
  lastPatchReceivedId: '',
  fanOn:'',
  minTemperature:'',
  maxTemperature:''
};
```
* 將包含報告屬性的修補程式傳送至中樞
```
// Send the reported properties patch to the hub
function sendReportedProperties() {
  twin.properties.reported.update(reportedPropertiesPatch, function(err) {
    if (err) throw err;
    console.log(chalk.blue('\nTwin state reported'));
    console.log(JSON.stringify(reportedPropertiesPatch, null, 2));
  });
}
```
* 後端應用程式會透過裝置對應項，存取裝置目前的報告屬性值
```
// Display the reported properties from the device
function printReportedProperties(twin) {
  console.log("Last received patch: " + twin.properties.reported.lastPatchReceivedId);
  console.log("Firmware version: " + twin.properties.reported.firmwareVersion);
  console.log("Fan status: " + twin.properties.reported.fanOn);
  console.log("Min temperature set: " + twin.properties.reported.minTemperature);
  console.log("Max temperature set: " + twin.properties.reported.maxTemperature);
}
```

#### 執行應用程式
同上一步驟中的 "執行應用程式"
![](https://i.imgur.com/tHezTjb.png)
![](https://i.imgur.com/IsEDaus.png)


