nanoFramework.Azure.Devices.Client
1.0.0-preview.123
Prefix Reserved
See the version list below for details.
dotnet add package nanoFramework.Azure.Devices.Client --version 1.0.0-preview.123
NuGet\Install-Package nanoFramework.Azure.Devices.Client -Version 1.0.0-preview.123
<PackageReference Include="nanoFramework.Azure.Devices.Client" Version="1.0.0-preview.123" />
paket add nanoFramework.Azure.Devices.Client --version 1.0.0-preview.123
#r "nuget: nanoFramework.Azure.Devices.Client, 1.0.0-preview.123"
// Install nanoFramework.Azure.Devices.Client as a Cake Addin #addin nuget:?package=nanoFramework.Azure.Devices.Client&version=1.0.0-preview.123&prerelease // Install nanoFramework.Azure.Devices.Client as a Cake Tool #tool nuget:?package=nanoFramework.Azure.Devices.Client&version=1.0.0-preview.123&prerelease
Welcome to the .NET nanoFramework Azure.Devices.Client Library repository
Build status
Component | Build Status | NuGet Package |
---|---|---|
nanoFramework.Azure.Devices.Client | ||
nanoFramework.Azure.Devices.Client (preview) |
See it in action
You can watch this video from the Microsoft IoT Show featuring the Azure SDK and a real life example with .NET nanoFramework:
Usage
Important: You must be connected to a network with a valid IP address and a valid date. Please check the examples with the Network Helpers on how to help you making sure you have both.
This Azure IoT Hub SDK is using MQTT. So you need to ensure you can connect to port 8883 using TLS protocol. If you are connected to an enterprise network, this may be blocked. In most cases, this is not an issue.
The namespaces, the name of the classes and the methods try to get close to the .NET C# Azure IoT SDK. This should allow easier portability of the code between the full .Net framework and nanoFramework environments.
Certificate
You have 2 options to provide the right Azure IoT TLS certificate:
- Parse it into the constructor
- Store it into the device
The AzureCertificates contains, for your convenience, the root certificate used to connect to Azure IoT. The current one, a Baltimore Root CA is the one to use up to June 2022. Starting from June 2022, the Digicert Global Root 2 is the one to use. For more information, please read the following blog.
Through the constructor
You will have to embed the certificate into your code:
const string AzureRootCA = @"-----BEGIN CERTIFICATE-----
MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ
RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD
VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX
DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y
ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy
VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr
mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr
IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK
mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu
XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy
dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye
jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1
BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3
DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92
9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx
jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0
Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz
ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS
R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp
-----END CERTIFICATE-----
";
DeviceClient azureIoT = new DeviceClient(IotBrokerAddress, DeviceID, SasKey, azureCert: new X509Certificate(AzureRootCA));
You can place your binary certificate in the program resources as well and just get the certificate from it:
X509Certificate azureRootCACert = new X509Certificate(Resources.GetBytes(Resources.BinaryResources.AzureCAcertificate));
DeviceClient azureIoT = new DeviceClient(IotBrokerAddress, DeviceID, SasKey, azureCert: azureRootCACert);
Note: when the certificate expires, you will have to fully reflash the device with the new certificate.
Storing the certificate on the device
You can store the certificate on the device flash and not in the code, so if you have to change the certificate, you'll just have to clean the current store and upload the new one. Edit the network properties:
Navigate to the General
tab:
Browse to choose your certificate, it can be in a binary (crt, der) or string form (pem, txt) and select ok. The certificate to connect will be selected automatically during the connection.
Creating a DeviceClient
You can connect to Azure IoT Hub using either a symmetric Key or a certificate. The following example shows how to use a symmetric key:
const string DeviceID = "nanoEdgeTwin";
const string IotBrokerAddress = "youriothub.azure-devices.net";
const string SasKey = "yoursaskey";
DeviceClient azureIoT = new DeviceClient(IotBrokerAddress, DeviceID, SasKey);
Note: please see the previous section to understand how to better parse the certificate for your usage. The example shows the certificate uploaded into the device and not in the code.
Azure IoT Plug&Play
Azure IoT Plug&Play is supported as well. You'll need to provide a model ID when creating the DeviceClient:
DeviceClient azureIoT = new DeviceClient(IotBrokerAddress, DeviceID, SasKey, modelID:"dtmi:com:example:Thermostat;1");
Note: the model ID has to be passed at the creation of the DeviceClient, it is not possible to pass it later on.
Reporting properties
Reporting Plug & Play properties is supported. He is a comprehensive example and how you can check if you have received one property that you're interested in:
const string TargetTemerature = "targetTemperature";
DeviceClient azureIoT = new DeviceClient(Secrets.IotHub, Secrets.DeviceName, Secrets.SasKey, azureCert: new X509Certificate(Resource.GetBytes(Resource.BinaryResources.AzureRoot)), modelId: "dtmi:com:example:Thermostat;1");
azureIoT.TwinUpated += AzureTwinUpdated;
azureIoT.Open();
void AzureTwinUpdated(object sender, TwinUpdateEventArgs e)
{
if (e.Twin.Contains(TargetTemerature))
{
// We got an update for the target temperature
var target = e.Twin[TargetTemerature];
Debug.WriteLine($"Target temperature updated: {target}");
PropertyAcknowledge targetReport = new() { Version = (int)e.Twin.Version, Status = PropertyStatus.Completed, Description = "All perfect", Value = target };
TwinCollection twin = new TwinCollection();
twin.Add(TargetTemerature, targetReport.BuildAcknowledge());
azureIoT.UpdateReportedProperties(twin);
}
}
In this example, the property we are interested in to receive is called targetTemperature
. To receive its update, we are subscribing to the twin update. And we can get the value thu the e.Twin[TargetTemerature]
once we've checked that the property exist.
The patter to publish a writable property is then quite simple. it's about building a PropertyAcknowledge
, creating a TwinCollection, adding it to it with the property name, here our targetTemperature
. You can add more properties to report of course. Note that what you add to the TwinCollection is not directly the object but BuildAcknowledge()
. One done, just ask the library to update the twin through the UpdateReportedProperties
method.
Receiving commands
An IoT Plug & Play command is a method callback. See further in this document how you can use them. In our case, the method is called getMaxMinReport
. The name of the method in C# must be the exact same as the name from the DTDL file.
DeviceClient azureIoT = new DeviceClient(Secrets.IotHub, Secrets.DeviceName, Secrets.SasKey, azureCert: new X509Certificate(Resource.GetBytes(Resource.BinaryResources.AzureRoot)), modelId: "dtmi:com:example:Thermostat;1");
azureIoT.AddMethodCallback(getMaxMinReport);
azureIoT.Open();
string getMaxMinReport(int rid, string payload)
{
TemperatureReporting reporting = new() { avgTemp = 20, maxTemp = 42, minTemp = 12.34, startTime = DateTime.UtcNow.AddDays(-10), endTime = DateTime.UtcNow };
return JsonConvert.SerializeObject(reporting);
}
In this example, the expected result is an object. Just populate the object and serialize it as a json as the command expect and return it. If any parameter to this command, it will be in the payload.
Getting and updating Twin
You can request your Azure IoT Twin simply by calling the GetTwin
function.
var twin = azureIoT.GetTwin(new CancellationTokenSource(20000).Token);
if (twin == null)
{
Debug.WriteLine($"Can't get the twins");
azureIoT.Close();
return;
}
Debug.WriteLine($"Twin DeviceID: {twin.DeviceId}, #desired: {twin.Properties.Desired.Count}, #reported: {twin.Properties.Reported.Count}");
Note: it's important to use a CancellationToken
that will be cancelled after a certain amount of time. Otherwise, this will be blocking the thread up to the point the twin is received.
Twins have properties, reported and desired. They are collection and you can get or try to get any element.
You can report your Twin as simple as this:
TwinCollection reported = new TwinCollection();
reported.Add("firmware", "myNano");
reported.Add("sdk", 0.2);
azureIoT.UpdateReportedProperties(reported);
You also have the option to wait for the twin update confirmation, in this case use a CancellationToken
that can be cancelled. Otherwise the check will be ignored.
Note: the function will return false if the twin reception confirmation is not checked or if it did not arrive on time.
You can also register for any twin update:
azureIoT.TwinUpated += TwinUpdatedEvent;
void TwinUpdatedEvent(object sender, TwinUpdateEventArgs e)
{
Debug.WriteLine($"Twin update received: {e.Twin.Count}");
}
Sending message
You have to use the SendMessage
function to send any kind of message or telemetry to Azure IoT. As for the other function, you have the possibility to ensure delivery with a CancellationToken
than can be cancelled. If one that can't be cancelled is used, the delivery insurance will be ignored and the function will return false.
var isReceived = azureIoT.SendMessage($"{{\"Temperature\":42,\"Pressure\":1024}}", new CancellationTokenSource(5000).Token);
Debug.WriteLine($"Message received by IoT Hub: {isReceived}");
Note: The message will be sent with the default service quality of service you created the device with. You won't get any answer for the quality 0
. In this case, you can simplify it to:
azureIoT.SendMessage($"{{\"Temperature\":42,\"Pressure\":1024}}");
Cloud to device messages
You can register an event to receive Cloud to device messages:
azureIoT.CloudToDeviceMessage += CloudToDeviceMessageEvent;
// The following example shows how to display all keys in debug
void CloudToDeviceMessageEvent(object sender, CloudToDeviceMessageEventArgs e)
{
Debug.WriteLine($"Message arrived: {e.Message}");
foreach (string key in e.Properties.Keys)
{
Debug.Write($" Key: {key} = ");
if (e.Properties[key] == null)
{
Debug.WriteLine("null");
}
else
{
Debug.WriteLine((string)e.Properties[key]);
}
}
// e.Message contains the message itself
if(e.Message == "stop")
{
ShoudIStop = true;
}
}
Note: the sender
is a DeviceClient
class, you can then send a message back, a confirmation or any logic you've put in place.
Method callback
Method callback is supported as well. You can register and unregister your methods. Here are a few examples:
azureIoT.AddMethodCallback(MethodCallbackTest);
azureIoT.AddMethodCallback(MakeAddition);
azureIoT.AddMethodCallback(RaiseExceptionCallbackTest);
string MethodCallbackTest(int rid, string payload)
{
Debug.WriteLine($"Call back called :-) rid={rid}, payload={payload}");
return "{\"Yes\":\"baby\",\"itisworking\":42}";
}
string MakeAddition(int rid, string payload)
{
Hashtable variables = (Hashtable)JsonConvert.DeserializeObject(payload, typeof(Hashtable));
int arg1 = (int)variables["arg1"];
int arg2 = (int)variables["arg2"];
return $"{{\"result\":{arg1 + arg2}}}";
}
string RaiseExceptionCallbackTest(int rid, string payload)
{
// This will properly return as well the exception error
throw new Exception("I got you, it's to test the 504");
}
Important: method names are case sensitive. So make sure you name your functions in C# use the same case.
Status update event
A status update event is available:
azureIoT.StatusUpdated += StatusUpdatedEvent;
void StatusUpdatedEvent(object sender, StatusUpdatedEventArgs e)
{
Debug.WriteLine($"Status changed: {e.IoTHubStatus.Status}, {e.IoTHubStatus.Message}");
// You may want to reconnect or use a similar retry mechanism
////if (e.IoTHubStatus.Status == Status.Disconnected)
////{
//// mqtt.Open();
////}
}
Note that those are status change based, so once the connect or disconnect event arrives, they'll be replaced by other events as soon as something else happened like receiving a twin.
Azure IoT Device Provisioning Service (DPS) support
This SDK also supports the Azure IoT Device Provisioning Service. Group and individual provisioning scenarios are supported either with a symmetric key either with certificates. To understand the mechanism behind DPS, it is recommended to read the documentation.
Provisioning using symmetric key
For symmetric key provisioning you only need the following elements:
- A registration ID
- The ID Scope
- The device name
- The key or the derived key for group provisioning
The code is then straight forward:
const string RegistrationID = "nanoDPStTest";
const string DpsAddress = "global.azure-devices-provisioning.net";
const string IdScope = "0ne01234567";
const string SasKey = "alongkeyencodedbase64";
// See the previous sections in the SDK help, you either need to have the Azure certificate embedded
// Either passing it in the constructor
X509Certificate azureCA = new X509Certificate(DpsSampleApp.Resources.GetBytes(DpsSampleApp.Resources.BinaryResources.BaltimoreRootCA_crt));
var provisioning = ProvisioningDeviceClient.Create(DpsAddress, IdScope, RegistrationID, SasKey, azureCA);
var myDevice = provisioning.Register(new CancellationTokenSource(60000).Token);
if(myDevice.Status != ProvisioningRegistrationStatusType.Assigned)
{
Debug.WriteLine($"Registration is not assigned: {myDevice.Status}, error message: {myDevice.ErrorMessage}");
return;
}
// You can then create the device
var device = new DeviceClient(myDevice.AssignedHub, myDevice.DeviceId, SasKey, nanoFramework.M2Mqtt.Messages.MqttQoSLevel.AtMostOnce, azureCA);
// Open it and continue like for the previous sections
var res = device.Open();
if(!res)
{
Debug.WriteLine($"can't open the device");
return;
}
Note: like for the DeviceClient
you need to make sure you are connected to a network properly and also have a proper data and time set on the device.
Provisioning using certificates
For symmetric key provisioning you only need the following elements:
- A registration ID
- The ID Scope
- The device name
- The device certificate
- Make sure that your IoT Hub is as well aware of the root/intermediate certificate you are using otherwise you won't be able to connect to your IoT Hub once your device is provisioned
The code is then straight forward:
const string RegistrationID = "nanoCertTest";
const string DpsAddress = "global.azure-devices-provisioning.net";
const string IdScope = "0ne0034F11A";
const string cert = @"
-----BEGIN CERTIFICATE-----
Your certificate
-----END CERTIFICATE-----
";
const string privateKey = @"
-----BEGIN ENCRYPTED PRIVATE KEY-----
the encrypted private key
-----END ENCRYPTED PRIVATE KEY-----
";
// See the previous sections in the SDK help, you either need to have the Azure certificate embedded
// Either passing it in the constructor
X509Certificate azureCA = new X509Certificate(DpsSampleApp.Resources.GetBytes(DpsSampleApp.Resources.BinaryResources.BaltimoreRootCA_crt));
// Note: if your private key is not protected with a password, you don't need to pass it
// You can as well store your certificate directly in the device certificate store
// And you can store it as a resource as well if needed
X509Certificate2 deviceCert = new X509Certificate2(cert, privateKey, "1234");
var provisioning = ProvisioningDeviceClient.Create(DpsAddress, IdScope, RegistrationID, deviceCert, azureCA);
var myDevice = provisioning.Register(new CancellationTokenSource(60000).Token);
if(myDevice.Status != ProvisioningRegistrationStatusType.Assigned)
{
Debug.WriteLine($"Registration is not assigned: {myDevice.Status}, error message: {myDevice.ErrorMessage}");
return;
}
// You can then create the device
var device = new DeviceClient(myDevice.AssignedHub, myDevice.DeviceId, deviceCert, nanoFramework.M2Mqtt.Messages.MqttQoSLevel.AtMostOnce, azureCA);
// Open it and continue like for the previous sections
var res = device.Open();
if(!res)
{
Debug.WriteLine($"can't open the device");
return;
}
Additional payload
Additional payload is supported as well. You can set it up as as json string in the ProvisioningRegistrationAdditionalData
class when calling the Register
function. When the device has been provisioned, you may have as well additional payload provided.
Feedback and documentation
For documentation, providing feedback, issues and finding out how to contribute please refer to the Home repo.
Join our Discord community here.
Credits
The list of contributors to this project can be found at CONTRIBUTORS.
License
The nanoFramework Class Libraries are licensed under the MIT license.
Code of Conduct
This project has adopted the code of conduct defined by the Contributor Covenant to clarify expected behaviour in our community. For more information see the .NET Foundation Code of Conduct.
.NET Foundation
This project is supported by the .NET Foundation.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET Framework | net is compatible. |
-
- nanoFramework.CoreLibrary (>= 1.11.7)
- nanoFramework.Json (>= 2.1.1)
- nanoFramework.M2Mqtt (>= 5.0.2-preview.45)
- nanoFramework.Runtime.Events (>= 1.10.0-preview.1)
- nanoFramework.Runtime.Native (>= 1.5.2)
- nanoFramework.System.Collections (>= 1.3.0)
- nanoFramework.System.Net (>= 1.7.2-preview.5)
- nanoFramework.System.Net.Http (>= 1.3.7-preview.4)
- nanoFramework.System.Text (>= 1.1.2)
- nanoFramework.System.Threading (>= 1.0.3)
- nanoFramework.Windows.Storage.Streams (>= 1.12.3)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on nanoFramework.Azure.Devices.Client:
Package | Downloads |
---|---|
MakoIoT.Device.Services.AzureIotHub
AzureIoTHub bus provider for MAKO-IoT |
GitHub repositories (2)
Showing the top 2 popular GitHub repositories that depend on nanoFramework.Azure.Devices.Client:
Repository | Stars |
---|---|
dotnet/samples
Sample code referenced by the .NET documentation
|
|
nanoframework/Samples
🍬 Code samples from the nanoFramework team used in testing, proof of concepts and other explorational endeavours
|
Version | Downloads | Last updated |
---|---|---|
1.2.62 | 149 | 11/4/2024 |
1.2.59 | 279 | 10/11/2024 |
1.2.58 | 210 | 10/3/2024 |
1.2.56 | 216 | 9/26/2024 |
1.2.53 | 259 | 9/10/2024 |
1.2.50 | 294 | 7/30/2024 |
1.2.47 | 1,019 | 5/20/2024 |
1.2.45 | 200 | 5/13/2024 |
1.2.43 | 89 | 5/13/2024 |
1.2.41 | 304 | 4/30/2024 |
1.2.38 | 286 | 4/9/2024 |
1.2.36 | 125 | 4/9/2024 |
1.2.34 | 269 | 4/3/2024 |
1.2.32 | 117 | 4/3/2024 |
1.2.29 | 557 | 1/29/2024 |
1.2.25 | 157 | 1/26/2024 |
1.2.23 | 208 | 1/24/2024 |
1.2.21 | 159 | 1/22/2024 |
1.2.16 | 909 | 11/23/2023 |
1.2.14 | 326 | 11/10/2023 |
1.2.12 | 241 | 11/8/2023 |
1.2.10 | 370 | 10/23/2023 |
1.2.7 | 281 | 10/10/2023 |
1.2.5 | 138 | 10/10/2023 |
1.2.3 | 544 | 9/5/2023 |
1.2.1 | 162 | 9/4/2023 |
1.1.145 | 206 | 8/28/2023 |
1.1.143 | 174 | 8/28/2023 |
1.1.141 | 178 | 8/28/2023 |
1.1.139 | 201 | 8/8/2023 |
1.1.137 | 277 | 7/27/2023 |
1.1.135 | 160 | 7/27/2023 |
1.1.133 | 287 | 5/28/2023 |
1.1.125 | 449 | 2/17/2023 |
1.1.122 | 408 | 1/14/2023 |
1.1.119 | 363 | 12/28/2022 |
1.1.116 | 291 | 12/28/2022 |
1.1.114 | 325 | 12/28/2022 |
1.1.106 | 528 | 11/24/2022 |
1.1.104 | 327 | 11/23/2022 |
1.1.101 | 345 | 11/23/2022 |
1.1.97 | 369 | 11/15/2022 |
1.1.95 | 435 | 11/4/2022 |
1.1.92 | 413 | 10/28/2022 |
1.1.90 | 401 | 10/28/2022 |
1.1.88 | 377 | 10/27/2022 |
1.1.85 | 407 | 10/26/2022 |
1.1.83 | 426 | 10/26/2022 |
1.1.81 | 376 | 10/26/2022 |
1.1.79 | 400 | 10/25/2022 |
1.1.77 | 394 | 10/25/2022 |
1.1.75 | 395 | 10/25/2022 |
1.1.73 | 391 | 10/25/2022 |
1.1.71 | 382 | 10/24/2022 |
1.1.69 | 422 | 10/24/2022 |
1.1.67 | 427 | 10/24/2022 |
1.1.64 | 445 | 10/23/2022 |
1.1.62 | 443 | 10/23/2022 |
1.1.60 | 425 | 10/21/2022 |
1.1.58 | 460 | 10/15/2022 |
1.1.56 | 448 | 10/12/2022 |
1.1.54 | 438 | 10/10/2022 |
1.1.52 | 418 | 10/10/2022 |
1.1.50 | 413 | 10/8/2022 |
1.1.48 | 439 | 10/8/2022 |
1.1.45 | 500 | 9/23/2022 |
1.1.41 | 463 | 9/22/2022 |
1.1.38 | 458 | 9/22/2022 |
1.1.36 | 445 | 9/22/2022 |
1.1.33 | 499 | 9/18/2022 |
1.1.31 | 515 | 9/15/2022 |
1.1.29 | 485 | 9/15/2022 |
1.1.26 | 468 | 9/15/2022 |
1.1.23 | 476 | 9/9/2022 |
1.1.19 | 547 | 8/5/2022 |
1.1.17 | 455 | 8/5/2022 |
1.1.15 | 445 | 8/4/2022 |
1.1.14 | 461 | 8/4/2022 |
1.1.12 | 434 | 8/4/2022 |
1.1.10 | 427 | 8/4/2022 |
1.1.8 | 439 | 8/3/2022 |
1.1.6 | 429 | 8/3/2022 |
1.1.4 | 441 | 8/3/2022 |
1.1.2 | 428 | 8/3/2022 |
1.0.1.38 | 439 | 8/3/2022 |
1.0.1.36 | 472 | 7/26/2022 |
1.0.1.34 | 525 | 7/19/2022 |
1.0.1.32 | 543 | 6/13/2022 |
1.0.1.30 | 476 | 6/13/2022 |
1.0.1.28 | 461 | 6/8/2022 |
1.0.1.26 | 462 | 6/8/2022 |
1.0.1.22 | 483 | 5/26/2022 |
1.0.1.20 | 467 | 5/26/2022 |
1.0.1.18 | 453 | 5/21/2022 |
1.0.1.16 | 506 | 5/19/2022 |
1.0.1.14 | 452 | 5/18/2022 |
1.0.1.12 | 443 | 5/18/2022 |
1.0.1.10 | 437 | 5/18/2022 |
1.0.1.6 | 510 | 5/3/2022 |
1.0.1.4 | 433 | 5/3/2022 |
1.0.1 | 447 | 5/3/2022 |
1.0.0 | 580 | 3/30/2022 |
1.0.0-preview.255 | 129 | 3/28/2022 |
1.0.0-preview.252 | 126 | 3/28/2022 |
1.0.0-preview.250 | 127 | 3/28/2022 |
1.0.0-preview.246 | 141 | 3/17/2022 |
1.0.0-preview.244 | 131 | 3/17/2022 |
1.0.0-preview.242 | 122 | 3/17/2022 |
1.0.0-preview.240 | 132 | 3/15/2022 |
1.0.0-preview.238 | 130 | 3/15/2022 |
1.0.0-preview.236 | 183 | 3/11/2022 |
1.0.0-preview.234 | 132 | 3/8/2022 |
1.0.0-preview.232 | 139 | 3/8/2022 |
1.0.0-preview.230 | 118 | 3/4/2022 |
1.0.0-preview.228 | 117 | 3/3/2022 |
1.0.0-preview.226 | 124 | 3/2/2022 |
1.0.0-preview.224 | 159 | 2/28/2022 |
1.0.0-preview.222 | 159 | 2/24/2022 |
1.0.0-preview.220 | 123 | 2/23/2022 |
1.0.0-preview.217 | 124 | 2/17/2022 |
1.0.0-preview.215 | 119 | 2/17/2022 |
1.0.0-preview.213 | 126 | 2/17/2022 |
1.0.0-preview.211 | 120 | 2/15/2022 |
1.0.0-preview.209 | 128 | 2/8/2022 |
1.0.0-preview.207 | 136 | 2/6/2022 |
1.0.0-preview.205 | 131 | 2/6/2022 |
1.0.0-preview.203 | 142 | 2/4/2022 |
1.0.0-preview.202 | 147 | 2/4/2022 |
1.0.0-preview.200 | 137 | 2/4/2022 |
1.0.0-preview.198 | 139 | 2/4/2022 |
1.0.0-preview.196 | 136 | 2/4/2022 |
1.0.0-preview.194 | 139 | 2/1/2022 |
1.0.0-preview.192 | 138 | 1/28/2022 |
1.0.0-preview.190 | 132 | 1/28/2022 |
1.0.0-preview.188 | 137 | 1/28/2022 |
1.0.0-preview.186 | 140 | 1/28/2022 |
1.0.0-preview.184 | 138 | 1/28/2022 |
1.0.0-preview.182 | 143 | 1/28/2022 |
1.0.0-preview.180 | 136 | 1/28/2022 |
1.0.0-preview.178 | 140 | 1/28/2022 |
1.0.0-preview.176 | 130 | 1/25/2022 |
1.0.0-preview.174 | 126 | 1/22/2022 |
1.0.0-preview.172 | 126 | 1/21/2022 |
1.0.0-preview.170 | 126 | 1/21/2022 |
1.0.0-preview.168 | 124 | 1/21/2022 |
1.0.0-preview.166 | 135 | 1/21/2022 |
1.0.0-preview.164 | 128 | 1/21/2022 |
1.0.0-preview.162 | 134 | 1/21/2022 |
1.0.0-preview.160 | 131 | 1/21/2022 |
1.0.0-preview.158 | 130 | 1/21/2022 |
1.0.0-preview.156 | 131 | 1/21/2022 |
1.0.0-preview.154 | 131 | 1/21/2022 |
1.0.0-preview.152 | 130 | 1/21/2022 |
1.0.0-preview.150 | 138 | 1/14/2022 |
1.0.0-preview.148 | 132 | 1/14/2022 |
1.0.0-preview.146 | 134 | 1/14/2022 |
1.0.0-preview.143 | 132 | 1/12/2022 |
1.0.0-preview.141 | 136 | 1/11/2022 |
1.0.0-preview.139 | 134 | 1/11/2022 |
1.0.0-preview.136 | 135 | 1/6/2022 |
1.0.0-preview.134 | 139 | 1/6/2022 |
1.0.0-preview.132 | 133 | 1/5/2022 |
1.0.0-preview.130 | 134 | 1/5/2022 |
1.0.0-preview.128 | 141 | 1/4/2022 |
1.0.0-preview.127 | 146 | 1/3/2022 |
1.0.0-preview.126 | 141 | 1/3/2022 |
1.0.0-preview.125 | 139 | 1/3/2022 |
1.0.0-preview.124 | 141 | 1/3/2022 |
1.0.0-preview.123 | 134 | 12/31/2021 |
1.0.0-preview.122 | 133 | 12/31/2021 |
1.0.0-preview.121 | 136 | 12/30/2021 |
1.0.0-preview.120 | 135 | 12/29/2021 |
1.0.0-preview.119 | 140 | 12/28/2021 |
1.0.0-preview.117 | 140 | 12/17/2021 |
1.0.0-preview.115 | 163 | 12/5/2021 |
1.0.0-preview.113 | 262 | 12/4/2021 |
1.0.0-preview.111 | 150 | 12/4/2021 |
1.0.0-preview.109 | 150 | 12/3/2021 |
1.0.0-preview.107 | 152 | 12/3/2021 |
1.0.0-preview.105 | 142 | 12/3/2021 |
1.0.0-preview.103 | 143 | 12/3/2021 |
1.0.0-preview.101 | 147 | 12/3/2021 |
1.0.0-preview.99 | 156 | 12/3/2021 |
1.0.0-preview.97 | 150 | 12/3/2021 |
1.0.0-preview.95 | 154 | 12/3/2021 |
1.0.0-preview.93 | 145 | 12/2/2021 |
1.0.0-preview.91 | 144 | 12/2/2021 |
1.0.0-preview.89 | 150 | 12/2/2021 |
1.0.0-preview.87 | 147 | 12/2/2021 |
1.0.0-preview.85 | 147 | 12/2/2021 |
1.0.0-preview.83 | 147 | 12/2/2021 |
1.0.0-preview.81 | 145 | 12/2/2021 |
1.0.0-preview.79 | 142 | 12/2/2021 |
1.0.0-preview.77 | 143 | 12/2/2021 |
1.0.0-preview.75 | 147 | 12/1/2021 |
1.0.0-preview.73 | 148 | 12/1/2021 |
1.0.0-preview.71 | 147 | 12/1/2021 |
1.0.0-preview.69 | 2,966 | 11/25/2021 |
1.0.0-preview.65 | 146 | 11/23/2021 |
1.0.0-preview.63 | 156 | 11/18/2021 |
1.0.0-preview.61 | 155 | 11/14/2021 |
1.0.0-preview.59 | 256 | 11/13/2021 |
1.0.0-preview.55 | 189 | 11/12/2021 |
1.0.0-preview.53 | 184 | 11/11/2021 |
1.0.0-preview.50 | 166 | 11/11/2021 |
1.0.0-preview.48 | 191 | 10/22/2021 |
1.0.0-preview.46 | 151 | 10/19/2021 |
1.0.0-preview.44 | 145 | 10/19/2021 |
1.0.0-preview.42 | 201 | 10/18/2021 |
1.0.0-preview.40 | 220 | 10/16/2021 |
1.0.0-preview.37 | 601 | 9/17/2021 |
1.0.0-preview.35 | 172 | 9/17/2021 |
1.0.0-preview.32 | 194 | 9/13/2021 |
1.0.0-preview.28 | 170 | 8/17/2021 |
1.0.0-preview.25 | 209 | 8/1/2021 |
1.0.0-preview.23 | 901 | 7/5/2021 |
1.0.0-preview.21 | 158 | 7/1/2021 |
1.0.0-preview.20 | 208 | 6/28/2021 |