From a6fec931649972258a9db76e8ad93f28f819b2ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Sim=C3=B5es?= Date: Tue, 11 Dec 2018 12:46:41 +0000 Subject: [PATCH] Work on update device config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Improve Monitor_UpdateConfiguration command to handle configuration blocks larger then the WP buffer size. - Add new configuration block to store X509 certificates. Signed-off-by: José Simões --- source/USB Test App WPF/MainWindow.xaml.cs | 54 ++++-- .../DeviceConfiguration.cs | 55 +++++- .../DeviceConfigurationBase.cs | 2 + .../DeviceConfigurationOption.cs | 5 + .../X509CertificateBase.cs | 33 ++++ .../X509CertificatePropertiesBase.cs | 27 +++ .../WireProtocol/Commands.cs | 24 ++- .../WireProtocol/Engine.cs | 165 ++++++++++++++++-- ...Framework.Tools.DebugLibrary.Net.projitems | 2 + 9 files changed, 334 insertions(+), 33 deletions(-) create mode 100644 source/nanoFramework.Tools.DebugLibrary.Shared/DeviceConfiguration/X509CertificateBase.cs create mode 100644 source/nanoFramework.Tools.DebugLibrary.Shared/DeviceConfiguration/X509CertificatePropertiesBase.cs diff --git a/source/USB Test App WPF/MainWindow.xaml.cs b/source/USB Test App WPF/MainWindow.xaml.cs index 1e3d9ac4..f5329870 100644 --- a/source/USB Test App WPF/MainWindow.xaml.cs +++ b/source/USB Test App WPF/MainWindow.xaml.cs @@ -27,6 +27,33 @@ namespace Serial_Test_App_WPF /// public partial class MainWindow : Window { + // Baltimore CyberTrust Root + // from https://cacert.omniroot.com/bc2025.crt + + // X509 RSA key PEM format 2048 bytes + private const string baltimoreCACertificate = +@"-----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-----"; + public MainWindow() { InitializeComponent(); @@ -757,10 +784,6 @@ private async void SetDeviceConfigButton_Click(object sender, RoutedEventArgs e) // get device info var deviceConfig = device.DebugEngine.GetDeviceConfiguration(cts.Token); - // change device configuration using the global configuration class - //deviceConfig.NetworkConfiguraton.MacAddress = new byte[] { 0, 0x80, 0xe1, 0x01, 0x35, 0x56 }; - //deviceConfig.NetworkConfiguraton.StartupAddressMode = DeviceConfiguration.AddressMode.DHCP; - // update new network configuration DeviceConfiguration.NetworkConfigurationProperties newDeviceNetworkConfiguration = new DeviceConfiguration.NetworkConfigurationProperties { @@ -774,16 +797,25 @@ private async void SetDeviceConfigButton_Click(object sender, RoutedEventArgs e) // write device configuration to device var returnValue = device.DebugEngine.UpdateDeviceConfiguration(newDeviceNetworkConfiguration, 0); - // add new wireless 802.11 configuration - DeviceConfiguration.Wireless80211ConfigurationProperties newWireless80211Configuration = new DeviceConfiguration.Wireless80211ConfigurationProperties() + //// add new wireless 802.11 configuration + //DeviceConfiguration.Wireless80211ConfigurationProperties newWireless80211Configuration = new DeviceConfiguration.Wireless80211ConfigurationProperties() + //{ + // Id = 44, + // Ssid = "Nice_Ssid", + // Password = "1234", + //}; + + //// write wireless configuration to device + //returnValue = device.DebugEngine.UpdateDeviceConfiguration(newWireless80211Configuration, 0); + + // add CA certificate + DeviceConfiguration.X509CertificateProperties newX509Certificate = new DeviceConfiguration.X509CertificateProperties() { - Id = 44, - Ssid = "Nice_Ssid", - Password = "1234", + Certificate = Encoding.UTF8.GetBytes(baltimoreCACertificate) }; - // write wireless configuration to device - returnValue = device.DebugEngine.UpdateDeviceConfiguration(newWireless80211Configuration, 0); + // write CA certificate to device + returnValue = device.DebugEngine.UpdateDeviceConfiguration(newX509Certificate, 0); Debug.WriteLine(""); Debug.WriteLine(""); diff --git a/source/nanoFramework.Tools.DebugLibrary.Shared/DeviceConfiguration/DeviceConfiguration.cs b/source/nanoFramework.Tools.DebugLibrary.Shared/DeviceConfiguration/DeviceConfiguration.cs index 290e96a1..918f40e2 100644 --- a/source/nanoFramework.Tools.DebugLibrary.Shared/DeviceConfiguration/DeviceConfiguration.cs +++ b/source/nanoFramework.Tools.DebugLibrary.Shared/DeviceConfiguration/DeviceConfiguration.cs @@ -35,6 +35,11 @@ public partial class DeviceConfiguration /// public static string MarkerConfigurationWireless80211AP_v1 = "AP1\0"; + /// + /// X509 Certificate configuration marker + /// + public static string MarkerConfigurationX509Certificate_v1 = "XC1\0"; + ///////////////////////////////////////////////////////////// /// @@ -47,19 +52,27 @@ public partial class DeviceConfiguration /// public List Wireless80211Configurations { get; set; } + /// + /// Collection of blocks in a target device. + /// + public List X509Certificates { get; set; } + public DeviceConfiguration() : this(new List(), - new List()) + new List(), + new List()) { } public DeviceConfiguration( List networkConfiguratons, - List networkWirelessConfiguratons + List networkWirelessConfiguratons, + List x509Certificates ) { NetworkConfigurations = networkConfiguratons; Wireless80211Configurations = networkWirelessConfiguratons; + X509Certificates = x509Certificates; } // operator to allow cast_ing a DeviceConfiguration object to DeviceConfigurationBase @@ -68,7 +81,8 @@ public static explicit operator DeviceConfigurationBase(DeviceConfiguration valu return new DeviceConfigurationBase() { NetworkConfigurations = value.NetworkConfigurations.Select(i => (NetworkConfigurationBase)i).ToArray(), - Wireless80211Configurations = value.Wireless80211Configurations.Select(i => (Wireless80211ConfigurationBase)i).ToArray() + Wireless80211Configurations = value.Wireless80211Configurations.Select(i => (Wireless80211ConfigurationBase)i).ToArray(), + X509Certificates = value.X509Certificates.Select(i => (X509CertificateBase)i).ToArray() }; } @@ -245,6 +259,41 @@ public static explicit operator Wireless80211ConfigurationBase(Wireless80211Conf } + + [AddINotifyPropertyChangedInterface] + public class X509CertificateProperties : X509CertificatePropertiesBase + { + public bool IsUnknown { get; set; } = true; + + public X509CertificateProperties() + { + + } + + public X509CertificateProperties(X509CertificateBase certificate) + { + CertificateSize = (uint)certificate.Certificate.Length; + Certificate = certificate.Certificate; + + // reset unknown flag + IsUnknown = false; + } + + // operator to allow cast_ing a X509CertificateBaseProperties object to X509CertificateBaseBase + public static explicit operator X509CertificateBase(X509CertificateProperties value) + { + var x509Certificate = new X509CertificateBase() + { + Marker = Encoding.UTF8.GetBytes(MarkerConfigurationX509Certificate_v1), + + CertificateSize = (uint)value.Certificate.Length, + Certificate = value.Certificate, + }; + + return x509Certificate; + } + } + ///////////////////////////////////////////////////////////// } diff --git a/source/nanoFramework.Tools.DebugLibrary.Shared/DeviceConfiguration/DeviceConfigurationBase.cs b/source/nanoFramework.Tools.DebugLibrary.Shared/DeviceConfiguration/DeviceConfigurationBase.cs index 966d9522..fd6534e9 100644 --- a/source/nanoFramework.Tools.DebugLibrary.Shared/DeviceConfiguration/DeviceConfigurationBase.cs +++ b/source/nanoFramework.Tools.DebugLibrary.Shared/DeviceConfiguration/DeviceConfigurationBase.cs @@ -11,5 +11,7 @@ public class DeviceConfigurationBase public NetworkConfigurationBase[] NetworkConfigurations; public Wireless80211ConfigurationBase[] Wireless80211Configurations { get; internal set; } + + public X509CertificateBase[] X509Certificates { get; internal set; } } } diff --git a/source/nanoFramework.Tools.DebugLibrary.Shared/DeviceConfiguration/DeviceConfigurationOption.cs b/source/nanoFramework.Tools.DebugLibrary.Shared/DeviceConfiguration/DeviceConfigurationOption.cs index d4613ac2..d5c00c9b 100644 --- a/source/nanoFramework.Tools.DebugLibrary.Shared/DeviceConfiguration/DeviceConfigurationOption.cs +++ b/source/nanoFramework.Tools.DebugLibrary.Shared/DeviceConfiguration/DeviceConfigurationOption.cs @@ -31,6 +31,11 @@ public enum DeviceConfigurationOption : byte /// WirelessNetworkAP = 3, + /// + /// X509 Certificate block + /// + X509Certificate = 4, + /// /// All configuration blocks /// diff --git a/source/nanoFramework.Tools.DebugLibrary.Shared/DeviceConfiguration/X509CertificateBase.cs b/source/nanoFramework.Tools.DebugLibrary.Shared/DeviceConfiguration/X509CertificateBase.cs new file mode 100644 index 00000000..685cbe93 --- /dev/null +++ b/source/nanoFramework.Tools.DebugLibrary.Shared/DeviceConfiguration/X509CertificateBase.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) 2018 The nanoFramework project contributors +// See LICENSE file in the project root for full license information. +// + +namespace nanoFramework.Tools.Debugger +{ + public class X509CertificateBase + { + /// + /// This is the marker placeholder for this configuration block + /// 4 bytes length. + /// + public byte[] Marker; + + /// + /// Size of the certificate. + /// + public uint CertificateSize; + + /// + /// Certificate + /// + public byte[] Certificate; + + public X509CertificateBase() + { + // need to init these here to match the expected size on the struct to be sent to the device + Marker = new byte[4]; + Certificate = new byte[64]; + } + } +} \ No newline at end of file diff --git a/source/nanoFramework.Tools.DebugLibrary.Shared/DeviceConfiguration/X509CertificatePropertiesBase.cs b/source/nanoFramework.Tools.DebugLibrary.Shared/DeviceConfiguration/X509CertificatePropertiesBase.cs new file mode 100644 index 00000000..226e75a4 --- /dev/null +++ b/source/nanoFramework.Tools.DebugLibrary.Shared/DeviceConfiguration/X509CertificatePropertiesBase.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) 2018 The nanoFramework project contributors +// See LICENSE file in the project root for full license information. +// + +using PropertyChanged; + +namespace nanoFramework.Tools.Debugger +{ + [AddINotifyPropertyChangedInterface] + public class X509CertificatePropertiesBase + { + private byte[] _certificate; + + public uint CertificateSize { get; set; } + + public byte[] Certificate + { + get => _certificate; + set + { + _certificate = value; + CertificateSize = (uint)value.Length; + } + } + } +} \ No newline at end of file diff --git a/source/nanoFramework.Tools.DebugLibrary.Shared/WireProtocol/Commands.cs b/source/nanoFramework.Tools.DebugLibrary.Shared/WireProtocol/Commands.cs index 5c56d182..47fcae7c 100644 --- a/source/nanoFramework.Tools.DebugLibrary.Shared/WireProtocol/Commands.cs +++ b/source/nanoFramework.Tools.DebugLibrary.Shared/WireProtocol/Commands.cs @@ -335,6 +335,23 @@ public void PrepareForDeserialize(int size, byte[] data, Converter converter) Password = new byte[64]; } } + + public class X509CertificateConfig : X509CertificateBase, IConverter + { + public X509CertificateConfig() + { + Marker = new byte[4]; + CertificateSize = 0xFFFF; + Certificate = new byte[64]; + } + + public void PrepareForDeserialize(int size, byte[] data, Converter converter) + { + Marker = new byte[4]; + CertificateSize = 0xFFFF; + Certificate = new byte[size - 4 - 4]; + } + } } public class Monitor_UpdateConfiguration @@ -342,6 +359,7 @@ public class Monitor_UpdateConfiguration public uint Configuration; public uint BlockIndex; public uint Length = 0; + public uint Offset = 0; public byte[] Data = null; public class Reply @@ -349,12 +367,14 @@ public class Reply public uint ErrorCode; }; - public void PrepareForSend(byte[] data, int length) + public void PrepareForSend(byte[] data, int length, int offset = 0) { Length = (uint)length; Data = new byte[length]; - Array.Copy(data, 0, Data, 0, length); + Offset = (uint)offset; + + Array.Copy(data, offset, Data, 0, length); } } diff --git a/source/nanoFramework.Tools.DebugLibrary.Shared/WireProtocol/Engine.cs b/source/nanoFramework.Tools.DebugLibrary.Shared/WireProtocol/Engine.cs index 2c8df383..c46a02af 100644 --- a/source/nanoFramework.Tools.DebugLibrary.Shared/WireProtocol/Engine.cs +++ b/source/nanoFramework.Tools.DebugLibrary.Shared/WireProtocol/Engine.cs @@ -3219,7 +3219,17 @@ public DeviceConfiguration GetDeviceConfiguration(CancellationToken cancellation return null; } - return new DeviceConfiguration(networkConfigs, networkWirelessConfigs); + // get all wireless network configuration blocks + var x509Certificates = GetAllX509Certificates(); + // check for cancellation request + if (cancellationToken.IsCancellationRequested) + { + // cancellation requested + Debug.WriteLine("cancellation requested"); + return null; + } + + return new DeviceConfiguration(networkConfigs, networkWirelessConfigs, x509Certificates); } public List GetAllNetworkConfigurations() @@ -3268,6 +3278,29 @@ public DeviceConfiguration GetDeviceConfiguration(CancellationToken cancellation return wireless80211Configurations; } + public List GetAllX509Certificates() + { + List x509Certificates = new List(); + + DeviceConfiguration.X509CertificateProperties x509CertificatesProperties = null; + uint index = 0; + + do + { + // get next X509 certificate configuration block, if available + x509CertificatesProperties = GetX509CertificatesProperties(index++); + + // add to list, if valid + if(!x509CertificatesProperties.IsUnknown) + { + x509Certificates.Add(x509CertificatesProperties); + } + } + while (!x509CertificatesProperties.IsUnknown); + + return x509Certificates; + } + public DeviceConfiguration.NetworkConfigurationProperties GetNetworkConfiguratonProperties(uint configurationBlockIndex) { Debug.WriteLine("NetworkConfiguratonProperties"); @@ -3321,6 +3354,29 @@ public DeviceConfiguration.Wireless80211ConfigurationProperties GetWireless80211 return wirelessConfigProperties; } + public DeviceConfiguration.X509CertificateProperties GetX509CertificatesProperties(uint configurationBlockIndex) + { + Debug.WriteLine("X509CertificateProperties"); + + IncomingMessage reply = GetDeviceConfiguration((uint)DeviceConfiguration.DeviceConfigurationOption.X509Certificate, configurationBlockIndex); + + Commands.Monitor_QueryConfiguration.X509CertificateConfig x509Certificate = new Commands.Monitor_QueryConfiguration.X509CertificateConfig(); + + DeviceConfiguration.X509CertificateProperties x509CertificateProperties = new DeviceConfiguration.X509CertificateProperties(); + + if (reply != null) + { + if (reply.Payload is Commands.Monitor_QueryConfiguration.Reply cmdReply && cmdReply.Data != null) + { + new Converter().Deserialize(x509Certificate, cmdReply.Data); + + x509CertificateProperties = new DeviceConfiguration.X509CertificateProperties(x509Certificate); + } + } + + return x509CertificateProperties; + } + private IncomingMessage GetDeviceConfiguration(uint configuration, uint configurationBlockIndex) { Commands.Monitor_QueryConfiguration cmd = new Commands.Monitor_QueryConfiguration @@ -3341,6 +3397,7 @@ private IncomingMessage GetDeviceConfiguration(uint configuration, uint configur public bool UpdateDeviceConfiguration(DeviceConfiguration configuration) { bool okToUploadConfig = false; + Commands.Monitor_FlashSectorMap.FlashSectorData configSector = new Commands.Monitor_FlashSectorMap.FlashSectorData(); // the requirement to erase flash before storing is dependent on CLR capabilities which is only available if the device is running nanoCLR // when running nanoBooter those are not available @@ -3362,7 +3419,7 @@ public bool UpdateDeviceConfiguration(DeviceConfiguration configuration) } // get configuration sector details - var configSector = FlashSectorMap.FirstOrDefault(item => (item.m_flags & Commands.Monitor_FlashSectorMap.c_MEMORY_USAGE_MASK) == Commands.Monitor_FlashSectorMap.c_MEMORY_USAGE_CONFIG); + configSector = FlashSectorMap.FirstOrDefault(item => (item.m_flags & Commands.Monitor_FlashSectorMap.c_MEMORY_USAGE_MASK) == Commands.Monitor_FlashSectorMap.c_MEMORY_USAGE_CONFIG); // check if the device has a config sector if (configSector.m_NumBlocks > 0) @@ -3396,27 +3453,89 @@ public bool UpdateDeviceConfiguration(DeviceConfiguration configuration) // serialize the configuration block var configurationSerialized = CreateConverter().Serialize(((DeviceConfigurationBase)configuration)); - // prepare command to upload new configuration - Commands.Monitor_UpdateConfiguration cmd = new Commands.Monitor_UpdateConfiguration - { - Configuration = (uint)DeviceConfiguration.DeviceConfigurationOption.All - }; - cmd.PrepareForSend(configurationSerialized, configurationSerialized.Length); + //// update by writing directly to the flash + //var writeConfigSector = WriteMemory(configSector.m_StartAddress, configurationSerialized); + + //if (!writeConfigSector.Success) + //{ + // // write failed, try to replace back the old config? + // // FIXME + //} - IncomingMessage reply = PerformSyncRequest(Commands.c_Monitor_UpdateConfiguration, 0, cmd); + // counters to manage the chunked update process + int count = configurationSerialized.Length; + int position = 0; - if (reply != null) + // flag to signal the update operation success/failure + bool updateFailed = true; + + while (count > 0) { - Commands.Monitor_UpdateConfiguration.Reply cmdReply = reply.Payload as Commands.Monitor_UpdateConfiguration.Reply; + Commands.Monitor_UpdateConfiguration cmd = new Commands.Monitor_UpdateConfiguration + { + Configuration = (uint)DeviceConfiguration.DeviceConfigurationOption.All + }; + + // get packet length, either the maximum allowed size or whatever is still available to TX + int packetLength = Math.Min((int)WireProtocolPacketSize, count); + + cmd.PrepareForSend(configurationSerialized, packetLength, position); - if (reply.IsPositiveAcknowledge() && cmdReply.ErrorCode == 0) + IncomingMessage reply = PerformSyncRequest(Commands.c_Monitor_UpdateConfiguration, 0, cmd); + + if (reply != null) { - return true; + Commands.Monitor_UpdateConfiguration.Reply cmdReply = reply.Payload as Commands.Monitor_UpdateConfiguration.Reply; + + if (!reply.IsPositiveAcknowledge() || cmdReply.ErrorCode != 0) + { + break; + } + + count -= packetLength; + position += packetLength; + + if(count == 0) + { + // update was OK, switch flag + updateFailed = false; + } } } - // write failed, try to replace back the old config? - // FIXME + if(updateFailed) + { + // failed to upload new configuration + // revert back old one + + // TODO + } + else + { + return true; + } + + ///////////////////////////////////// + //// current implementation without offset + //// prepare command to upload new configuration + //Commands.Monitor_UpdateConfiguration cmd = new Commands.Monitor_UpdateConfiguration + //{ + // Configuration = (uint)DeviceConfiguration.DeviceConfigurationOption.All + //}; + //cmd.PrepareForSend(configurationSerialized, configurationSerialized.Length); + + //IncomingMessage reply = PerformSyncRequest(Commands.c_Monitor_UpdateConfiguration, 0, cmd); + //if (reply != null) + //{ + // Commands.Monitor_UpdateConfiguration.Reply cmdReply = reply.Payload as Commands.Monitor_UpdateConfiguration.Reply; + + // if (reply.IsPositiveAcknowledge() && cmdReply.ErrorCode == 0) + // { + // return true; + // } + // // write failed, try to replace back the old config? + // // FIXME + //} } // default to false @@ -3469,15 +3588,27 @@ public bool UpdateDeviceConfiguration(T configuration, uint blockIndex) currentConfiguration.Wireless80211Configurations[(int)blockIndex] = configuration as DeviceConfiguration.Wireless80211ConfigurationProperties; } } + else if (configuration.GetType().Equals(typeof(DeviceConfiguration.X509CertificateProperties))) + { + // if list is empty and request index is 0 + if (currentConfiguration.X509Certificates.Count == 0 && blockIndex == 0) + { + currentConfiguration.X509Certificates.Add(configuration as DeviceConfiguration.X509CertificateProperties); + } + else + { + currentConfiguration.X509Certificates[(int)blockIndex] = configuration as DeviceConfiguration.X509CertificateProperties; + } + } - if(UpdateDeviceConfiguration(currentConfiguration)) + if (UpdateDeviceConfiguration(currentConfiguration)) { // done here return true; } else { - // write failed, the old configuration is supposed to have been reverted by + // write failed, the old configuration it's supposed to have been reverted by now } } diff --git a/source/nanoFramework.Tools.DebugLibrary.Shared/nanoFramework.Tools.DebugLibrary.Net.projitems b/source/nanoFramework.Tools.DebugLibrary.Shared/nanoFramework.Tools.DebugLibrary.Net.projitems index 6b95b6c5..36c1fc45 100644 --- a/source/nanoFramework.Tools.DebugLibrary.Shared/nanoFramework.Tools.DebugLibrary.Net.projitems +++ b/source/nanoFramework.Tools.DebugLibrary.Shared/nanoFramework.Tools.DebugLibrary.Net.projitems @@ -17,6 +17,8 @@ + +