diff options
| author | Roy Ben Shabat <Roy.mail.net@gmail.com> | 2025-08-19 23:12:47 +0300 |
|---|---|---|
| committer | Roy Ben Shabat <Roy.mail.net@gmail.com> | 2025-08-19 23:12:47 +0300 |
| commit | 9d2bca0f44fe0a4a2f25e819f6c27974181f5221 (patch) | |
| tree | 89be962f45a377b1bca5b967c7223aacb13e89ae | |
| parent | 4fd21b08a24f1697ab6159509cad4fadf66c1a32 (diff) | |
| download | Tango-9d2bca0f44fe0a4a2f25e819f6c27974181f5221.tar.gz Tango-9d2bca0f44fe0a4a2f25e819f6c27974181f5221.zip | |
Machine Service Telemetry Checkpoints & Device Registration.
12 files changed, 480 insertions, 57 deletions
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<Tango.PPC.Common.Web.MachineVersionsRequest, Tango.PPC.Common.Web.MachineVersionsResponse>("GetMachineVersions", request); } + /// <summary> + /// Executes the SetTelemetryCheckPoints action and returns Tango.PPC.Common.Web.TelemetrySetCheckPointsResponse. + /// </summary> + /// <returns></returns> + public Task<Tango.PPC.Common.Web.TelemetrySetCheckPointsResponse> SetTelemetryCheckPoints(Tango.PPC.Common.Web.TelemetrySetCheckPointsRequest request) + { + return Post<Tango.PPC.Common.Web.TelemetrySetCheckPointsRequest, Tango.PPC.Common.Web.TelemetrySetCheckPointsResponse>("SetTelemetryCheckPoints", request); + } + + /// <summary> + /// Executes the GetTelemetryCheckPoints action and returns Tango.PPC.Common.Web.TelemetryGetCheckPointsResponse. + /// </summary> + /// <returns></returns> + public Task<Tango.PPC.Common.Web.TelemetryGetCheckPointsResponse> GetTelemetryCheckPoints(Tango.PPC.Common.Web.TelemetryGetCheckPointsRequest request) + { + return Post<Tango.PPC.Common.Web.TelemetryGetCheckPointsRequest, Tango.PPC.Common.Web.TelemetryGetCheckPointsResponse>("GetTelemetryCheckPoints", request); + } + + /// <summary> + /// Executes the GetTelemetryDeviceConnection action and returns Tango.PPC.Common.Web.TelemetryDeviceRegistrationResponse. + /// </summary> + /// <returns></returns> + public Task<Tango.PPC.Common.Web.TelemetryDeviceRegistrationResponse> GetTelemetryDeviceConnection(Tango.PPC.Common.Web.TelemetryDeviceRegistrationRequest request) + { + return Post<Tango.PPC.Common.Web.TelemetryDeviceRegistrationRequest, Tango.PPC.Common.Web.TelemetryDeviceRegistrationResponse>("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<TelemetryCheckPoint> 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<PPC.Common.Web.LoginRequest, PPC.Common.Web.LoginResponse, MachineService.Controllers.PPCController>("Tango.PPC.Common.Web", "PPCWebClientBase", PathHelper.GetSolutionFolder() + @"\PPC\Tango.PPC.Common\Web"); + GenerateWebClient<PPC.Common.Web.LoginRequest, PPC.Common.Web.LoginResponse, MachineService.Controllers.PPCController>("Tango.PPC.Common.Web", "PPCWebClientBase", PathHelper.GetSolutionFolder() + @"\PPC\Tango.PPC.Common\Web"); //Generate Machine Studio client. //GenerateWebClient<MachineStudio.Common.Web.LoginRequest, MachineStudio.Common.Web.LoginResponse, MachineService.Controllers.MachineStudioController>("Tango.MachineStudio.Common.Web", "MachineStudioWebClientBase", PathHelper.GetSolutionFolder() + @"\MachineStudio\Tango.MachineStudio.Common\Web"); //Generate FSE client. - GenerateWebClientV2<FSE.Web.Messages.LoginRequest, FSE.Web.Messages.LoginResponse, MachineService.Controllers.FSEController>("Tango.FSE.BL.Web", "FSEWebClientBase", PathHelper.GetSolutionFolder() + @"\FSE\Tango.FSE.BL\Web"); + //GenerateWebClientV2<FSE.Web.Messages.LoginRequest, FSE.Web.Messages.LoginResponse, MachineService.Controllers.FSEController>("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<DownloadModel> downloads = new List<DownloadModel>(); + 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<DownloadModel> downloads = new List<DownloadModel>(); - 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 @@ <Reference Include="Microsoft.AspNet.SignalR.SystemWeb, Version=2.4.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> <HintPath>..\..\packages\Microsoft.AspNet.SignalR.SystemWeb.2.4.1\lib\net45\Microsoft.AspNet.SignalR.SystemWeb.dll</HintPath> </Reference> + <Reference Include="Microsoft.Azure.Amqp, Version=2.4.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <HintPath>..\..\packages\Microsoft.Azure.Amqp.2.5.12\lib\net45\Microsoft.Azure.Amqp.dll</HintPath> + </Reference> <Reference Include="Microsoft.Azure.Common, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" /> <Reference Include="Microsoft.Azure.Common.NetFramework, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL" /> + <Reference Include="Microsoft.Azure.Devices, Version=1.39.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <HintPath>..\..\packages\Microsoft.Azure.Devices.1.39.0\lib\net451\Microsoft.Azure.Devices.dll</HintPath> + </Reference> + <Reference Include="Microsoft.Azure.Devices.Shared, Version=1.30.3.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <HintPath>..\..\packages\Microsoft.Azure.Devices.Shared.1.30.3\lib\net451\Microsoft.Azure.Devices.Shared.dll</HintPath> + </Reference> <Reference Include="Microsoft.Azure.Management.Sql, Version=0.9.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL" /> <Reference Include="Microsoft.Azure.ResourceManager, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL" /> <Reference Include="Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=1.0.3.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> @@ -79,8 +88,8 @@ <Reference Include="Microsoft.IdentityModel.Clients.ActiveDirectory.WindowsForms, Version=2.7.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> <HintPath>..\..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.2.7.10707.1513-rc\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.WindowsForms.dll</HintPath> </Reference> - <Reference Include="Microsoft.Owin, Version=4.0.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> - <HintPath>..\..\packages\Microsoft.Owin.4.0.1\lib\net45\Microsoft.Owin.dll</HintPath> + <Reference Include="Microsoft.Owin, Version=4.2.2.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <HintPath>..\..\packages\Microsoft.Owin.4.2.2\lib\net45\Microsoft.Owin.dll</HintPath> </Reference> <Reference Include="Microsoft.Owin.Cors, Version=4.0.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> <HintPath>..\..\packages\Microsoft.Owin.Cors.4.0.1\lib\net45\Microsoft.Owin.Cors.dll</HintPath> @@ -210,7 +219,15 @@ </Reference> <Reference Include="System.Drawing" /> <Reference Include="System.IdentityModel" /> + <Reference Include="System.Net" /> + <Reference Include="System.Net.Http" /> + <Reference Include="System.Numerics" /> + <Reference Include="System.Runtime, Version=4.1.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" /> <Reference Include="System.Security" /> + <Reference Include="System.Transactions" /> + <Reference Include="System.ValueTuple, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL"> + <HintPath>..\..\packages\System.ValueTuple.4.5.0\lib\net461\System.ValueTuple.dll</HintPath> + </Reference> <Reference Include="System.Web.Cors, Version=5.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> <HintPath>..\..\packages\Microsoft.AspNet.Cors.5.0.0\lib\net45\System.Web.Cors.dll</HintPath> </Reference> @@ -230,8 +247,6 @@ <Private>True</Private> <HintPath>..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll</HintPath> </Reference> - <Reference Include="System.Net.Http"> - </Reference> <Reference Include="System.Net.Http.Formatting, Version=5.2.3.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> <HintPath>..\..\packages\Microsoft.AspNet.WebApi.Client.5.2.3\lib\net45\System.Net.Http.Formatting.dll</HintPath> </Reference> @@ -346,6 +361,8 @@ </Compile> <Compile Include="Models\SessionVariables.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> + <Compile Include="Telemetry\TelemetryCheckpointStore.cs" /> + <Compile Include="Telemetry\TelemetryDeviceRegistrationManager.cs" /> <Compile Include="Views\FSEAccount\ResetPasswordVM.cs" /> <Compile Include="Views\FSEDownloads\IndexViewModel.cs" /> <Content Include="Images\eureka.png" /> @@ -508,7 +525,7 @@ <SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile> </WebProjectProperties> </FlavorProperties> - <UserProperties BuildVersion_AssemblyInfoFilename="Properties\AssemblyInfo.cs" BuildVersion_UpdateAssemblyVersion="True" BuildVersion_BuildVersioningStyle="None.None.Increment.TimeStamp" BuildVersion_UseGlobalSettings="False" BuildVersion_StartDate="2000/1/1" /> + <UserProperties BuildVersion_StartDate="2000/1/1" BuildVersion_UseGlobalSettings="False" BuildVersion_BuildVersioningStyle="None.None.Increment.TimeStamp" BuildVersion_UpdateAssemblyVersion="True" BuildVersion_AssemblyInfoFilename="Properties\AssemblyInfo.cs" /> </VisualStudio> </ProjectExtensions> <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild"> 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("?", "_"); + } + + /// <summary> + /// Store/get telemetry source checkpoints in Azure Table Storage. + /// PartitionKey = MachineGuid, RowKey = SourceName. + /// </summary> + public sealed class TelemetryCheckpointStore + { + private readonly CloudTable _table; + + /// <param name="connectionString">Azure Storage connection string</param> + /// <param name="tableName">Table name (default: TelemetryCheckPoints)</param> + 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 + } + + /// <summary>Upsert a single checkpoint for a machine + source (synchronous).</summary> + 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); + } + + /// <summary>Upsert up to N checkpoints in batches of 100 (Table Storage limit) — synchronous.</summary> + public void SaveMany(string machineGuid, IEnumerable<TelemetryCheckPoint> 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); + } + } + + /// <summary>Get the checkpoint for a specific machine + source. Returns null if not found. (sync)</summary> + 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<TelemetryCheckpointEntity>(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 + }; + } + + /// <summary>Get all checkpoints for a machine (sync). Uses ExecuteQuery which handles paging internally.</summary> + public IReadOnlyList<TelemetryCheckPoint> GetAllForMachine(string machineGuid) + { + if (string.IsNullOrWhiteSpace(machineGuid)) throw new ArgumentNullException(nameof(machineGuid)); + + var query = new TableQuery<TelemetryCheckpointEntity>() + .Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, machineGuid)); + + // ExecuteQuery is synchronous and iterates all segments internally + var entities = _table.ExecuteQuery(query); + + var list = new List<TelemetryCheckPoint>(); + 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; + } + + /// <summary>Delete a specific checkpoint (sync). No-op if not found.</summary> + 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<TelemetryCheckpointEntity>(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("?", "_"); + } + + /// <summary> + /// Creates/gets IoT Hub device identities and caches the per-device connection string + /// in Azure Table Storage ("TelemetryDeviceRegistrations"). + /// PartitionKey = MachineGuid, RowKey = SerialNumber (sanitized). + /// </summary> + 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 + } + + /// <summary> + /// 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. + /// </summary> + 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<TelemetryDeviceRegistrationEntity>(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 @@ <add key="TWILIO_FROM_NUMBER" value="+16466815605" /> <add key="TWILIO_ENABLE_SMS" value="true" /> <add key="TWILIO_ENABLE_ALPHA_SENDER" value="true" /> + + <add key="IOT_HUB_CONNECTION_STRING" value="HostName=iot-twine-dev-weu.azure-devices.net;SharedAccessKeyName=iothubowner;SharedAccessKey=PUcTQPd+5dCkc9sHqlDO2P3fV/wDlr4VNAIoTFqVovY=" /> </appSettings> <!-- @@ -198,7 +200,7 @@ </dependentAssembly> <dependentAssembly> <assemblyIdentity name="Microsoft.Owin" publicKeyToken="31bf3856ad364e35" culture="neutral" /> - <bindingRedirect oldVersion="0.0.0.0-4.0.1.0" newVersion="4.0.1.0" /> + <bindingRedirect oldVersion="0.0.0.0-4.2.2.0" newVersion="4.2.2.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="Z.EntityFramework.Extensions" publicKeyToken="59b66d028979105b" culture="neutral" /> diff --git a/Software/Visual_Studio/Web/Tango.MachineService/packages.config b/Software/Visual_Studio/Web/Tango.MachineService/packages.config index ff05c3063..a944a6267 100644 --- a/Software/Visual_Studio/Web/Tango.MachineService/packages.config +++ b/Software/Visual_Studio/Web/Tango.MachineService/packages.config @@ -26,10 +26,14 @@ <package id="Microsoft.AspNet.WebApi.HelpPage" version="5.2.3" targetFramework="net45" /> <package id="Microsoft.AspNet.WebApi.WebHost" version="5.2.3" targetFramework="net45" /> <package id="Microsoft.AspNet.WebPages" version="3.2.3" targetFramework="net45" /> + <package id="Microsoft.Azure.Amqp" version="2.5.12" targetFramework="net461" /> + <package id="Microsoft.Azure.Devices" version="1.39.0" targetFramework="net461" /> + <package id="Microsoft.Azure.Devices.Shared" version="1.30.3" targetFramework="net461" /> <package id="Microsoft.CodeDom.Providers.DotNetCompilerPlatform" version="1.0.3" targetFramework="net45" /> + <package id="Microsoft.CSharp" version="4.7.0" targetFramework="net461" /> <package id="Microsoft.IdentityModel.Clients.ActiveDirectory" version="5.0.5" targetFramework="net461" /> <package id="Microsoft.Net.Compilers" version="2.4.0" targetFramework="net46" developmentDependency="true" /> - <package id="Microsoft.Owin" version="4.0.1" targetFramework="net461" /> + <package id="Microsoft.Owin" version="4.2.2" targetFramework="net461" /> <package id="Microsoft.Owin.Cors" version="4.0.1" targetFramework="net461" /> <package id="Microsoft.Owin.Host.SystemWeb" version="2.1.0" targetFramework="net461" /> <package id="Microsoft.Owin.Security" version="2.1.0" targetFramework="net461" /> @@ -43,6 +47,7 @@ <package id="System.Data.SQLite.Core" version="1.0.108.0" targetFramework="net46" /> <package id="System.Data.SQLite.EF6" version="1.0.108.0" targetFramework="net46" /> <package id="System.Data.SQLite.Linq" version="1.0.108.0" targetFramework="net46" /> + <package id="System.ValueTuple" version="4.5.0" targetFramework="net461" /> <package id="Twilio" version="7.6.0" targetFramework="net461" /> <package id="WebGrease" version="1.5.2" targetFramework="net45" /> <package id="Z.EntityFramework.Extensions" version="5.1.6" targetFramework="net461" /> |
