From 9d2bca0f44fe0a4a2f25e819f6c27974181f5221 Mon Sep 17 00:00:00 2001 From: Roy Ben Shabat Date: Tue, 19 Aug 2025 23:12:47 +0300 Subject: Machine Service Telemetry Checkpoints & Device Registration. --- .../PPC/Tango.PPC.Common/Web/PPCWebClientBase.cs | 27 ++++ .../Web/TelemetryGetCheckPointsResponse.cs | 2 +- .../Tango.Telemetry.Tester.IOT.CLI/Program.cs | 3 +- .../Utilities/Tango.WebClientGenerator/Program.cs | 4 +- .../Controllers/DownloadsController.cs | 93 ++++++------ .../Controllers/PPCController.cs | 52 +++++++ .../Tango.MachineService/MachineServiceConfig.cs | 3 + .../Tango.MachineService.csproj | 27 +++- .../Telemetry/TelemetryCheckpointStore.cs | 161 +++++++++++++++++++++ .../TelemetryDeviceRegistrationManager.cs | 154 ++++++++++++++++++++ .../Web/Tango.MachineService/Web.config | 4 +- .../Web/Tango.MachineService/packages.config | 7 +- 12 files changed, 480 insertions(+), 57 deletions(-) create mode 100644 Software/Visual_Studio/Web/Tango.MachineService/Telemetry/TelemetryCheckpointStore.cs create mode 100644 Software/Visual_Studio/Web/Tango.MachineService/Telemetry/TelemetryDeviceRegistrationManager.cs (limited to 'Software/Visual_Studio') diff --git a/Software/Visual_Studio/PPC/Tango.PPC.Common/Web/PPCWebClientBase.cs b/Software/Visual_Studio/PPC/Tango.PPC.Common/Web/PPCWebClientBase.cs index 0363285f8..d0fb21150 100644 --- a/Software/Visual_Studio/PPC/Tango.PPC.Common/Web/PPCWebClientBase.cs +++ b/Software/Visual_Studio/PPC/Tango.PPC.Common/Web/PPCWebClientBase.cs @@ -175,5 +175,32 @@ namespace Tango.PPC.Common.Web return Post("GetMachineVersions", request); } + /// + /// Executes the SetTelemetryCheckPoints action and returns Tango.PPC.Common.Web.TelemetrySetCheckPointsResponse. + /// + /// + public Task SetTelemetryCheckPoints(Tango.PPC.Common.Web.TelemetrySetCheckPointsRequest request) + { + return Post("SetTelemetryCheckPoints", request); + } + + /// + /// Executes the GetTelemetryCheckPoints action and returns Tango.PPC.Common.Web.TelemetryGetCheckPointsResponse. + /// + /// + public Task GetTelemetryCheckPoints(Tango.PPC.Common.Web.TelemetryGetCheckPointsRequest request) + { + return Post("GetTelemetryCheckPoints", request); + } + + /// + /// Executes the GetTelemetryDeviceConnection action and returns Tango.PPC.Common.Web.TelemetryDeviceRegistrationResponse. + /// + /// + public Task GetTelemetryDeviceConnection(Tango.PPC.Common.Web.TelemetryDeviceRegistrationRequest request) + { + return Post("GetTelemetryDeviceConnection", request); + } + } } diff --git a/Software/Visual_Studio/PPC/Tango.PPC.Common/Web/TelemetryGetCheckPointsResponse.cs b/Software/Visual_Studio/PPC/Tango.PPC.Common/Web/TelemetryGetCheckPointsResponse.cs index ff167acc5..72943f61c 100644 --- a/Software/Visual_Studio/PPC/Tango.PPC.Common/Web/TelemetryGetCheckPointsResponse.cs +++ b/Software/Visual_Studio/PPC/Tango.PPC.Common/Web/TelemetryGetCheckPointsResponse.cs @@ -7,7 +7,7 @@ using Tango.Transport.Web; namespace Tango.PPC.Common.Web { - public class TelemetryGetCheckPointsResponse : WebRequestMessage + public class TelemetryGetCheckPointsResponse : WebResponseMessage { public List Checkpoints { get; set; } diff --git a/Software/Visual_Studio/Utilities/Tango.Telemetry.Tester.IOT.CLI/Program.cs b/Software/Visual_Studio/Utilities/Tango.Telemetry.Tester.IOT.CLI/Program.cs index b11c75f6f..1b02b9066 100644 --- a/Software/Visual_Studio/Utilities/Tango.Telemetry.Tester.IOT.CLI/Program.cs +++ b/Software/Visual_Studio/Utilities/Tango.Telemetry.Tester.IOT.CLI/Program.cs @@ -42,7 +42,8 @@ namespace Tango.Telemetry.Tester.IOT.CLI //publisher.RegisterSource(new JobRunsTestSource()); //publisher.RegisterSource(new DiagnosticsTestSource()); publisher.RegisterSource(new EventsTestSource()); - publisher.RegisterDestination(new TelemetryAzureHubDestination("HostName=iot-twine-dev-weu.azure-devices.net;DeviceId=telemetry-dev-01;SharedAccessKey=cZhCMhiVL+TF7p13fpX+lFmyxoy8ZqCkbxUwumWw18Q=")); + //publisher.RegisterDestination(new TelemetryAzureHubDestination("HostName=iot-twine-dev-weu.azure-devices.net;DeviceId=telemetry-dev-01;SharedAccessKey=cZhCMhiVL+TF7p13fpX+lFmyxoy8ZqCkbxUwumWw18Q=")); + publisher.RegisterDestination(new TelemetryMqttDestination("Telemetry MQTT")); publisher.PublishResultAvailable += Publisher_PublishResultAvailable; publisher.PublishingPackage += Publisher_PublishingPackage; diff --git a/Software/Visual_Studio/Utilities/Tango.WebClientGenerator/Program.cs b/Software/Visual_Studio/Utilities/Tango.WebClientGenerator/Program.cs index 501058016..d7567d73d 100644 --- a/Software/Visual_Studio/Utilities/Tango.WebClientGenerator/Program.cs +++ b/Software/Visual_Studio/Utilities/Tango.WebClientGenerator/Program.cs @@ -17,13 +17,13 @@ namespace Tango.WebClientGenerator static void Main(string[] args) { //Generate PPC client. - //GenerateWebClient("Tango.PPC.Common.Web", "PPCWebClientBase", PathHelper.GetSolutionFolder() + @"\PPC\Tango.PPC.Common\Web"); + GenerateWebClient("Tango.PPC.Common.Web", "PPCWebClientBase", PathHelper.GetSolutionFolder() + @"\PPC\Tango.PPC.Common\Web"); //Generate Machine Studio client. //GenerateWebClient("Tango.MachineStudio.Common.Web", "MachineStudioWebClientBase", PathHelper.GetSolutionFolder() + @"\MachineStudio\Tango.MachineStudio.Common\Web"); //Generate FSE client. - GenerateWebClientV2("Tango.FSE.BL.Web", "FSEWebClientBase", PathHelper.GetSolutionFolder() + @"\FSE\Tango.FSE.BL\Web"); + //GenerateWebClientV2("Tango.FSE.BL.Web", "FSEWebClientBase", PathHelper.GetSolutionFolder() + @"\FSE\Tango.FSE.BL\Web"); Console.WriteLine("Done"); } diff --git a/Software/Visual_Studio/Web/Tango.MachineService/Controllers/DownloadsController.cs b/Software/Visual_Studio/Web/Tango.MachineService/Controllers/DownloadsController.cs index b2de177aa..c99cfcf6d 100644 --- a/Software/Visual_Studio/Web/Tango.MachineService/Controllers/DownloadsController.cs +++ b/Software/Visual_Studio/Web/Tango.MachineService/Controllers/DownloadsController.cs @@ -21,62 +21,63 @@ namespace Tango.MachineService.Controllers { public class DownloadsController : Controller { - [Authorize] public ActionResult Index() { - List downloads = new List(); + return new RedirectResult("https://twine-srv.com/"); - using (ObservablesContext db = ObservablesWebContext.CreateContext()) - { - foreach (var item in db.MachineStudioVersions.Where(x => x.InstallerBlobName != null).Include(x => x.User).Include(x => x.User.Contact).ToList()) - { - DownloadModel download = new DownloadModel(); - download.App = DownloadModel.DownloadApp.MachineStudio; - download.ID = item.InstallerBlobName; - download.Name = $"Machine Studio v{item.Version}.exe"; - download.Version = item.Version; - download.User = item.User.Contact.FullName; - download.Date = item.LastUpdated; - download.Comments = item.Comments; + //List downloads = new List(); - downloads.Add(download); - } + //using (ObservablesContext db = ObservablesWebContext.CreateContext()) + //{ + // foreach (var item in db.MachineStudioVersions.Where(x => x.InstallerBlobName != null).Include(x => x.User).Include(x => x.User.Contact).ToList()) + // { + // DownloadModel download = new DownloadModel(); + // download.App = DownloadModel.DownloadApp.MachineStudio; + // download.ID = item.InstallerBlobName; + // download.Name = $"Machine Studio v{item.Version}.exe"; + // download.Version = item.Version; + // download.User = item.User.Contact.FullName; + // download.Date = item.LastUpdated; + // download.Comments = item.Comments; - foreach (var item in db.TangoVersions.Where(x => x.InstallerBlobName != null).Include(x => x.User).Include(x => x.User.Contact).Include(x => x.MachineVersion).Where(x => x.MachineVersion.Version == 1).ToList()) - { - DownloadModel download = new DownloadModel(); - download.App = DownloadModel.DownloadApp.PPC; - download.ID = item.InstallerBlobName; - download.Name = $"PPC v{item.VersionAndTag}.exe"; - download.Version = item.Version; - download.User = item.User.Contact.FullName; - download.Date = item.LastUpdated; - download.Comments = item.Comments; + // downloads.Add(download); + // } - downloads.Add(download); - } + // foreach (var item in db.TangoVersions.Where(x => x.InstallerBlobName != null).Include(x => x.User).Include(x => x.User.Contact).Include(x => x.MachineVersion).Where(x => x.MachineVersion.Version == 1).ToList()) + // { + // DownloadModel download = new DownloadModel(); + // download.App = DownloadModel.DownloadApp.PPC; + // download.ID = item.InstallerBlobName; + // download.Name = $"PPC v{item.VersionAndTag}.exe"; + // download.Version = item.Version; + // download.User = item.User.Contact.FullName; + // download.Date = item.LastUpdated; + // download.Comments = item.Comments; - foreach (var item in db.TangoVersions.Where(x => x.InstallerBlobName != null).Include(x => x.User).Include(x => x.User.Contact).Include(x => x.MachineVersion).Where(x => x.MachineVersion.Version == 2).ToList()) - { - DownloadModel download = new DownloadModel(); - download.App = DownloadModel.DownloadApp.Eureka; - download.ID = item.InstallerBlobName; - download.Name = $"Twine4X v{item.VersionAndTag}.exe"; - download.Version = item.Version; - download.User = item.User.Contact.FullName; - download.Date = item.LastUpdated; - download.Comments = item.Comments; + // downloads.Add(download); + // } - downloads.Add(download); - } - } + // foreach (var item in db.TangoVersions.Where(x => x.InstallerBlobName != null).Include(x => x.User).Include(x => x.User.Contact).Include(x => x.MachineVersion).Where(x => x.MachineVersion.Version == 2).ToList()) + // { + // DownloadModel download = new DownloadModel(); + // download.App = DownloadModel.DownloadApp.Eureka; + // download.ID = item.InstallerBlobName; + // download.Name = $"Twine4X v{item.VersionAndTag}.exe"; + // download.Version = item.Version; + // download.User = item.User.Contact.FullName; + // download.Date = item.LastUpdated; + // download.Comments = item.Comments; + + // downloads.Add(download); + // } + //} - downloads = downloads.OrderByDescending(x => x.Date).ToList(); + //downloads = downloads.OrderByDescending(x => x.Date).ToList(); - IndexViewModel model = new IndexViewModel(); - model.Downloads = downloads; + //IndexViewModel model = new IndexViewModel(); + //model.Downloads = downloads; - return View(model); + //return View(model); } [Authorize] @@ -100,4 +101,4 @@ namespace Tango.MachineService.Controllers return Redirect(signature); } } -} \ No newline at end of file +} \ No newline at end of file diff --git a/Software/Visual_Studio/Web/Tango.MachineService/Controllers/PPCController.cs b/Software/Visual_Studio/Web/Tango.MachineService/Controllers/PPCController.cs index b60bc77c0..41266cda5 100644 --- a/Software/Visual_Studio/Web/Tango.MachineService/Controllers/PPCController.cs +++ b/Software/Visual_Studio/Web/Tango.MachineService/Controllers/PPCController.cs @@ -28,6 +28,10 @@ using Z.EntityFramework.Plus; using Twilio; using Twilio.Rest.Api.V2010.Account; using Twilio.Types; +using Microsoft.WindowsAzure.Storage; +using Tango.Web; +using Microsoft.WindowsAzure.Storage.Table; +using Tango.MachineService.Telemetry; namespace Tango.MachineService.Controllers { @@ -1114,5 +1118,53 @@ namespace Tango.MachineService.Controllers } #endregion + + #region Telemetry + + [HttpPost] + [JwtTokenFilter] + public TelemetrySetCheckPointsResponse SetTelemetryCheckPoints(TelemetrySetCheckPointsRequest request) + { + TelemetryCheckpointStore store = new TelemetryCheckpointStore(MachineServiceConfig.STORAGE_ACCOUNT); + store.SaveMany(RequestToken.Object.MachineGuid, request.Checkpoints); + return new TelemetrySetCheckPointsResponse(); + } + + [HttpPost] + [JwtTokenFilter] + public TelemetryGetCheckPointsResponse GetTelemetryCheckPoints(TelemetryGetCheckPointsRequest request) + { + TelemetryCheckpointStore store = new TelemetryCheckpointStore(MachineServiceConfig.STORAGE_ACCOUNT); + var checkPoints = store.GetAllForMachine(RequestToken.Object.MachineGuid).ToList(); + return new TelemetryGetCheckPointsResponse() { Checkpoints = checkPoints }; + } + + [HttpPost] + [JwtTokenFilter] + public TelemetryDeviceRegistrationResponse GetTelemetryDeviceConnection(TelemetryDeviceRegistrationRequest request) + { + var response = new TelemetryDeviceRegistrationResponse(); + + using (ObservablesContext db = ObservablesWebContext.CreateContext()) + { + var machine = db.Machines.SingleOrDefault(x => x.Guid == RequestToken.Object.MachineGuid); + if (machine == null) + throw new AuthenticationException("The specified machine could not be found."); + + string serialNumber = machine.SerialNumber; + string storageAccount = MachineServiceConfig.STORAGE_ACCOUNT; // Azure Storage CS + string iotHubService = MachineServiceConfig.IOT_HUB_CONNECTION_STRING; // iothubowner SAS (service) CS + + var mgr = new TelemetryDeviceRegistrationManager(storageAccount, iotHubService); + + string iotHubDeviceConnectionString = mgr.GetOrCreateDeviceConnectionString(machine.Guid, serialNumber); + + response.ConnectionString = iotHubDeviceConnectionString; + } + + return response; + } + + #endregion } } diff --git a/Software/Visual_Studio/Web/Tango.MachineService/MachineServiceConfig.cs b/Software/Visual_Studio/Web/Tango.MachineService/MachineServiceConfig.cs index 15c0637d6..8fb510941 100644 --- a/Software/Visual_Studio/Web/Tango.MachineService/MachineServiceConfig.cs +++ b/Software/Visual_Studio/Web/Tango.MachineService/MachineServiceConfig.cs @@ -32,5 +32,8 @@ namespace Tango.MachineService public static String TWILIO_FROM_NUMBER => ConfigurationManager.AppSettings[nameof(TWILIO_FROM_NUMBER)].ToString(); public static bool TWILIO_ENABLE_SMS => bool.Parse(ConfigurationManager.AppSettings[nameof(TWILIO_ENABLE_SMS)].ToString()); public static bool TWILIO_ENABLE_ALPHA_SENDER => bool.Parse(ConfigurationManager.AppSettings[nameof(TWILIO_ENABLE_ALPHA_SENDER)].ToString()); + + public static String IOT_HUB_CONNECTION_STRING => ConfigurationManager.AppSettings[nameof(IOT_HUB_CONNECTION_STRING)].ToString(); + } } \ No newline at end of file diff --git a/Software/Visual_Studio/Web/Tango.MachineService/Tango.MachineService.csproj b/Software/Visual_Studio/Web/Tango.MachineService/Tango.MachineService.csproj index 9f8889d60..e9c766c5d 100644 --- a/Software/Visual_Studio/Web/Tango.MachineService/Tango.MachineService.csproj +++ b/Software/Visual_Studio/Web/Tango.MachineService/Tango.MachineService.csproj @@ -65,8 +65,17 @@ ..\..\packages\Microsoft.AspNet.SignalR.SystemWeb.2.4.1\lib\net45\Microsoft.AspNet.SignalR.SystemWeb.dll + + ..\..\packages\Microsoft.Azure.Amqp.2.5.12\lib\net45\Microsoft.Azure.Amqp.dll + + + ..\..\packages\Microsoft.Azure.Devices.1.39.0\lib\net451\Microsoft.Azure.Devices.dll + + + ..\..\packages\Microsoft.Azure.Devices.Shared.1.30.3\lib\net451\Microsoft.Azure.Devices.Shared.dll + @@ -79,8 +88,8 @@ ..\..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.2.7.10707.1513-rc\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.WindowsForms.dll - - ..\..\packages\Microsoft.Owin.4.0.1\lib\net45\Microsoft.Owin.dll + + ..\..\packages\Microsoft.Owin.4.2.2\lib\net45\Microsoft.Owin.dll ..\..\packages\Microsoft.Owin.Cors.4.0.1\lib\net45\Microsoft.Owin.Cors.dll @@ -210,7 +219,15 @@ + + + + + + + ..\..\packages\System.ValueTuple.4.5.0\lib\net461\System.ValueTuple.dll + ..\..\packages\Microsoft.AspNet.Cors.5.0.0\lib\net45\System.Web.Cors.dll @@ -230,8 +247,6 @@ True ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll - - ..\..\packages\Microsoft.AspNet.WebApi.Client.5.2.3\lib\net45\System.Net.Http.Formatting.dll @@ -346,6 +361,8 @@ + + @@ -508,7 +525,7 @@ False - + diff --git a/Software/Visual_Studio/Web/Tango.MachineService/Telemetry/TelemetryCheckpointStore.cs b/Software/Visual_Studio/Web/Tango.MachineService/Telemetry/TelemetryCheckpointStore.cs new file mode 100644 index 000000000..1cd40e523 --- /dev/null +++ b/Software/Visual_Studio/Web/Tango.MachineService/Telemetry/TelemetryCheckpointStore.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.WindowsAzure.Storage; +using Microsoft.WindowsAzure.Storage.Table; +using Tango.PPC.Common.Web; + +namespace Tango.MachineService.Telemetry +{ + internal class TelemetryCheckpointEntity : TableEntity + { + public TelemetryCheckpointEntity() { } + + public TelemetryCheckpointEntity(string machineGuid, string sourceName) + { + PartitionKey = machineGuid; + RowKey = SanitizeRowKey(sourceName); + } + + public DateTime Time { get; set; } + public int TotalCount { get; set; } + + internal static string SanitizeRowKey(string value) => + (value ?? string.Empty) + .Replace("/", "_").Replace("\\", "_") + .Replace("#", "_").Replace("?", "_"); + } + + /// + /// Store/get telemetry source checkpoints in Azure Table Storage. + /// PartitionKey = MachineGuid, RowKey = SourceName. + /// + public sealed class TelemetryCheckpointStore + { + private readonly CloudTable _table; + + /// Azure Storage connection string + /// Table name (default: TelemetryCheckPoints) + public TelemetryCheckpointStore(string connectionString, string tableName = "TelemetryCheckPoints") + { + if (string.IsNullOrWhiteSpace(connectionString)) throw new ArgumentNullException(nameof(connectionString)); + if (string.IsNullOrWhiteSpace(tableName)) throw new ArgumentNullException(nameof(tableName)); + + var storageAccount = CloudStorageAccount.Parse(connectionString); + var tableClient = storageAccount.CreateCloudTableClient(); + + _table = tableClient.GetTableReference(tableName); + _table.CreateIfNotExists(); // sync + } + + /// Upsert a single checkpoint for a machine + source (synchronous). + public void Save(string machineGuid, TelemetryCheckPoint checkpoint) + { + if (string.IsNullOrWhiteSpace(machineGuid)) throw new ArgumentNullException(nameof(machineGuid)); + if (checkpoint == null) throw new ArgumentNullException(nameof(checkpoint)); + if (string.IsNullOrWhiteSpace(checkpoint.SourceName)) throw new ArgumentNullException(nameof(checkpoint.SourceName)); + + var entity = new TelemetryCheckpointEntity(machineGuid, checkpoint.SourceName) + { + Time = checkpoint.Time, + TotalCount = checkpoint.TotalCount + }; + + var op = TableOperation.InsertOrReplace(entity); + _table.Execute(op); + } + + /// Upsert up to N checkpoints in batches of 100 (Table Storage limit) — synchronous. + public void SaveMany(string machineGuid, IEnumerable checkpoints) + { + if (string.IsNullOrWhiteSpace(machineGuid)) throw new ArgumentNullException(nameof(machineGuid)); + if (checkpoints == null) return; + + var batch = new TableBatchOperation(); + foreach (var cp in checkpoints) + { + if (cp == null || string.IsNullOrWhiteSpace(cp.SourceName)) continue; + + var entity = new TelemetryCheckpointEntity(machineGuid, cp.SourceName) + { + Time = cp.Time, + TotalCount = cp.TotalCount + }; + + batch.InsertOrReplace(entity); + + if (batch.Count == 100) + { + _table.ExecuteBatch(batch); + batch.Clear(); + } + } + + if (batch.Count > 0) + { + _table.ExecuteBatch(batch); + } + } + + /// Get the checkpoint for a specific machine + source. Returns null if not found. (sync) + public TelemetryCheckPoint Get(string machineGuid, string sourceName) + { + if (string.IsNullOrWhiteSpace(machineGuid)) throw new ArgumentNullException(nameof(machineGuid)); + if (string.IsNullOrWhiteSpace(sourceName)) throw new ArgumentNullException(nameof(sourceName)); + + var rowKey = TelemetryCheckpointEntity.SanitizeRowKey(sourceName); + var op = TableOperation.Retrieve(machineGuid, rowKey); + var result = _table.Execute(op); + + var entity = result.Result as TelemetryCheckpointEntity; + return entity == null ? null : new TelemetryCheckPoint + { + SourceName = sourceName, + Time = entity.Time, + TotalCount = entity.TotalCount + }; + } + + /// Get all checkpoints for a machine (sync). Uses ExecuteQuery which handles paging internally. + public IReadOnlyList GetAllForMachine(string machineGuid) + { + if (string.IsNullOrWhiteSpace(machineGuid)) throw new ArgumentNullException(nameof(machineGuid)); + + var query = new TableQuery() + .Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, machineGuid)); + + // ExecuteQuery is synchronous and iterates all segments internally + var entities = _table.ExecuteQuery(query); + + var list = new List(); + foreach (var e in entities) + { + list.Add(new TelemetryCheckPoint + { + // RowKey is sanitized; ensure you pass sanitized name when saving & querying + SourceName = e.RowKey, + Time = e.Time, + TotalCount = e.TotalCount + }); + } + + return list; + } + + /// Delete a specific checkpoint (sync). No-op if not found. + public void Delete(string machineGuid, string sourceName) + { + if (string.IsNullOrWhiteSpace(machineGuid)) throw new ArgumentNullException(nameof(machineGuid)); + if (string.IsNullOrWhiteSpace(sourceName)) throw new ArgumentNullException(nameof(sourceName)); + + var rowKey = TelemetryCheckpointEntity.SanitizeRowKey(sourceName); + var retrieve = TableOperation.Retrieve(machineGuid, rowKey); + var result = _table.Execute(retrieve); + var entity = result.Result as TelemetryCheckpointEntity; + if (entity == null) return; + + var del = TableOperation.Delete(entity); + _table.Execute(del); + } + } +} diff --git a/Software/Visual_Studio/Web/Tango.MachineService/Telemetry/TelemetryDeviceRegistrationManager.cs b/Software/Visual_Studio/Web/Tango.MachineService/Telemetry/TelemetryDeviceRegistrationManager.cs new file mode 100644 index 000000000..0bf8d2bdc --- /dev/null +++ b/Software/Visual_Studio/Web/Tango.MachineService/Telemetry/TelemetryDeviceRegistrationManager.cs @@ -0,0 +1,154 @@ +using System; +using Microsoft.WindowsAzure.Storage; +using Microsoft.WindowsAzure.Storage.Table; // same SDK as your sample +using Microsoft.Azure.Devices; // Install-Package Microsoft.Azure.Devices +using Microsoft.Azure.Devices.Common.Exceptions; + +namespace Tango.MachineService.Telemetry +{ + internal sealed class TelemetryDeviceRegistrationEntity : TableEntity + { + public TelemetryDeviceRegistrationEntity() { } + + public TelemetryDeviceRegistrationEntity(string machineGuid, string serialNumber) + { + PartitionKey = machineGuid; + RowKey = SanitizeRowKey(serialNumber); + } + + public String Environment { get; set; } + public string SerialNumber { get; set; } + public string DeviceId { get; set; } + public string ConnectionString { get; set; } + public DateTime CreatedUtc { get; set; } + public DateTime UpdatedUtc { get; set; } + + internal static string SanitizeRowKey(string value) => + (value ?? string.Empty) + .Replace("/", "_").Replace("\\", "_") + .Replace("#", "_").Replace("?", "_"); + } + + /// + /// Creates/gets IoT Hub device identities and caches the per-device connection string + /// in Azure Table Storage ("TelemetryDeviceRegistrations"). + /// PartitionKey = MachineGuid, RowKey = SerialNumber (sanitized). + /// + public sealed class TelemetryDeviceRegistrationManager + { + private readonly string _iotHubServiceConnectionString; + private readonly string _iotHubHostName; + private readonly CloudTable _table; + private readonly object _locker = new object(); + + public TelemetryDeviceRegistrationManager( + string storageAccountConnectionString, + string iotHubServiceConnectionString, + string tableName = "TelemetryDeviceRegistrations") + { + if (string.IsNullOrWhiteSpace(storageAccountConnectionString)) + throw new ArgumentNullException(nameof(storageAccountConnectionString)); + if (string.IsNullOrWhiteSpace(iotHubServiceConnectionString)) + throw new ArgumentNullException(nameof(iotHubServiceConnectionString)); + if (string.IsNullOrWhiteSpace(tableName)) + throw new ArgumentNullException(nameof(tableName)); + + _iotHubServiceConnectionString = iotHubServiceConnectionString; + _iotHubHostName = ExtractHostName(iotHubServiceConnectionString); + + var storageAccount = CloudStorageAccount.Parse(storageAccountConnectionString); + var tableClient = storageAccount.CreateCloudTableClient(); + _table = tableClient.GetTableReference(tableName); + _table.CreateIfNotExists(); // sync, as in your sample + } + + /// + /// Returns the per-device IoT Hub connection string for (machineGuid, serialNumber). + /// Creates the IoT Hub device (DeviceId = serialNumber) if needed, persists the connection string, + /// then returns the cached value next time. + /// + public string GetOrCreateDeviceConnectionString(string machineGuid, string serialNumber) + { + if (string.IsNullOrWhiteSpace(machineGuid)) throw new ArgumentNullException(nameof(machineGuid)); + if (string.IsNullOrWhiteSpace(serialNumber)) throw new ArgumentNullException(nameof(serialNumber)); + + // 1) Try table cache + var cached = TryGetFromTable(machineGuid, serialNumber); + if (!string.IsNullOrEmpty(cached)) + return cached; + + // 2) Create/get in IoT Hub (guard against concurrent callers) + lock (_locker) + { + // double-check after lock + cached = TryGetFromTable(machineGuid, serialNumber); + if (!string.IsNullOrEmpty(cached)) + return cached; + + var registry = RegistryManager.CreateFromConnectionString(_iotHubServiceConnectionString); + + Device device; + try + { + device = registry.AddDeviceAsync(new Device(serialNumber)).GetAwaiter().GetResult(); + } + catch (DeviceAlreadyExistsException) + { + device = registry.GetDeviceAsync(serialNumber).GetAwaiter().GetResult(); + } + + if (device == null) + throw new InvalidOperationException("Failed to get or create IoT Hub device."); + + var primaryKey = device.Authentication?.SymmetricKey?.PrimaryKey; + if (string.IsNullOrEmpty(primaryKey)) + throw new InvalidOperationException("Device has no symmetric key."); + + var deviceConnectionString = + $"HostName={_iotHubHostName};DeviceId={serialNumber};SharedAccessKey={primaryKey}"; + + UpsertToTable(machineGuid, serialNumber, deviceConnectionString); + + return deviceConnectionString; + } + } + + // ---- Table helpers (sync) ---- + + private string TryGetFromTable(string machineGuid, string serialNumber) + { + var rowKey = TelemetryDeviceRegistrationEntity.SanitizeRowKey(serialNumber); + var retrieve = TableOperation.Retrieve(machineGuid, rowKey); + var result = _table.Execute(retrieve); + var entity = result.Result as TelemetryDeviceRegistrationEntity; + return entity?.ConnectionString; + } + + private void UpsertToTable(string machineGuid, string serialNumber, string connectionString) + { + var entity = new TelemetryDeviceRegistrationEntity(machineGuid, serialNumber) + { + SerialNumber = serialNumber, + DeviceId = serialNumber, + Environment = MachineServiceConfig.DEPLOYMENT_SLOT.ToString(), + ConnectionString = connectionString, + CreatedUtc = DateTime.UtcNow, + UpdatedUtc = DateTime.UtcNow + }; + + var op = TableOperation.InsertOrReplace(entity); + _table.Execute(op); + } + + // ---- Utility ---- + + private static string ExtractHostName(string iotHubServiceConnectionString) + { + var parts = iotHubServiceConnectionString.Split(';'); + foreach (var p in parts) + if (p.StartsWith("HostName=", StringComparison.OrdinalIgnoreCase)) + return p.Substring("HostName=".Length); + throw new ArgumentException("IoT Hub service connection string is missing HostName=…"); + } + } +} diff --git a/Software/Visual_Studio/Web/Tango.MachineService/Web.config b/Software/Visual_Studio/Web/Tango.MachineService/Web.config index 84b3d0a3a..85260c32c 100644 --- a/Software/Visual_Studio/Web/Tango.MachineService/Web.config +++ b/Software/Visual_Studio/Web/Tango.MachineService/Web.config @@ -45,6 +45,8 @@ + +