Mistral AI ASP Net CORE MinimalAPI Experiment

Over the last couple of months, I’ve been experimenting with a range of AI coding tools starting with GitHub Copilot, then Anthropic Claude, and more recently, Mistral (I was looking for an on-prem solution). Mistral is a French company so is covered by the General Data Protect Regulation(GDPR) rules of the European Union(EU) which is way better than the regulations the other providers have to comply with.

Like many .NET developers, I started with Copilot as a natural extension of my workflow, expecting it to streamline repetitive tasks and accelerate development. When I started using Building Edge AI with Github Copilot- Security Camera HTTP(Jan 2025) the experience wasn’t great. Especially when I was using it for the “niche” areas I work-in it was pretty hopeless (sometimes even referred me to my own blog posts).

After a while I started trialing the other tools in my workflow and though they were better, sometimes F2-Replace or intellisense were faster and used a lot less tokens. I would also get the different tools to review the code of the others. I especially liked the Claude “Irony stack” when using it review Co-Pilot generated code.

While the other tools certainly helped (especially after adding custom skills files), I often found myself spending as much time going “down rabbit holes”(not the tool’s problem, though I hopefully learnt some useful stuff) and correcting or restructuring or debugging generated code that I could have written faster from scratch.

That’s what made my “out of box” experience with Mistral stand out. With a relatively simple prompt, it produced code that was not only concise but surprisingly accurate with just a single compile time error and no warnings on the first pass.

NOTE: This was using the webby interface, but I now have a paid for subscription.

The instructions which included .NET 8 (bit retro) and “dotnet add package”(pretty good) meant the code compiled on second attempt. The issue was a syntax error initialising OpenTelemetry which was quickly fixed, somewhat ironically with GitHub Copilot.

.ConfigureResource(resourceBuilder) rather than .ConfigureResource(rb => rb = resourceBuilder)

//dotnet add package OpenTelemetry 
//dotnet add package OpenTelemetry.Extensions.Hosting
//dotnet add package OpenTelemetry.Instrumentation.AspNetCore
//dotnet add package OpenTelemetry.Instrumentation.Http
//dotnet add package OpenTelemetry.Exporter.Console
//dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
//
//using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using System.Diagnostics;

var builder = WebApplication.CreateBuilder(args);

// Configure OpenTelemetry with a resource (service name)
var resourceBuilder = ResourceBuilder.CreateDefault()
    .AddService(serviceName: builder.Environment.ApplicationName);

// Add OpenTelemetry Tracing
builder.Services.AddOpenTelemetry()
    //.ConfigureResource(resourceBuilder) /**** This was the only compile time issue
    .ConfigureResource(rb => rb = resourceBuilder)
    .WithTracing(tracerProviderBuilder =>
    {
       tracerProviderBuilder
           .AddSource("MinimalApiSample")
           .AddAspNetCoreInstrumentation(options =>
           {
              options.RecordException = true;
           })
           .AddHttpClientInstrumentation()
           .AddConsoleExporter(); // For demo: export to console
           //.AddOtlpExporter(); // Uncomment to export to OpenTelemetry Collector

    })
    .WithMetrics(metricsProviderBuilder =>
    {
       metricsProviderBuilder
           .AddAspNetCoreInstrumentation()
           .AddHttpClientInstrumentation()
           .AddConsoleExporter(); // For demo: export to console
           //.AddOtlpExporter(); // Uncomment to export to OpenTelemetry Collector
    });

var app = builder.Build();

// Example of a custom activity for tracing
var activitySource = new ActivitySource("MinimalApiSample");

app.MapGet("/", () =>
{
   using var activity = activitySource.StartActivity("RootEndpoint");
   activity?.SetTag("custom.tag", "Hello, OpenTelemetry!");
   return Results.Ok("Hello, OpenTelemetry!");
});

app.MapGet("/metrics", () =>
{
   // This endpoint is just for demo; metrics are exported automatically
   return Results.Ok("Metrics are being collected in the background.");
});

app.Run();
//dotnet add package OpenTelemetry
//dotnet add package OpenTelemetry.Extensions.Hosting
//dotnet add package OpenTelemetry.Instrumentation.AspNetCore
//dotnet add package OpenTelemetry.Instrumentation.Http
//dotnet add package Azure.Monitor.OpenTelemetry.Exporter
//
using Azure.Monitor.OpenTelemetry.Exporter; 
//using OpenTelemetry; 
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using System.Diagnostics;

var builder = WebApplication.CreateBuilder(args);

// Configure OpenTelemetry with a resource (service name)
var resourceBuilder = ResourceBuilder.CreateDefault()
    .AddService(serviceName: builder.Environment.ApplicationName)
    .AddTelemetrySdk();

// Add OpenTelemetry Tracing and Metrics for Azure Application Insights
builder.Services.AddOpenTelemetry()
    //.ConfigureResource(resourceBuilder)
    .ConfigureResource(rb => rb = resourceBuilder) //*****
    .WithTracing(tracerProviderBuilder =>
    {
       tracerProviderBuilder
           .AddSource("MinimalApiSample")
           .AddAspNetCoreInstrumentation(options =>
           {
              options.RecordException = true;
           })
           .AddHttpClientInstrumentation()
           .AddAzureMonitorTraceExporter(options =>
           {
              options.ConnectionString = builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"];
           });
    })
    .WithMetrics(metricsProviderBuilder =>
    {
       metricsProviderBuilder
           .AddAspNetCoreInstrumentation()
           .AddHttpClientInstrumentation()
           .AddAzureMonitorMetricExporter(options =>
           {
              options.ConnectionString = builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"];
           });
    });

var app = builder.Build();

// Example of a custom activity for tracing
var activitySource = new ActivitySource("MinimalApiSample");

app.MapGet("/", () =>
{
   using var activity = activitySource.StartActivity("RootEndpoint");
   activity?.SetTag("custom.tag", "Hello, Azure Application Insights!");
   return Results.Ok("Hello, Azure Application Insights!");
});

app.MapGet("/metrics", () =>
{
   return Results.Ok("Metrics and traces are being sent to Azure Application Insights.");
});

app.Run();

Using Application Insights metrics the Kestral.active_connections graphs to shows some of the additional telemetry emitted by the application.

//dotnet add package OpenTelemetry
//dotnet add package OpenTelemetry.Extensions.Hosting
//dotnet add package OpenTelemetry.Instrumentation.AspNetCore
//dotnet add package OpenTelemetry.Instrumentation.Http
//dotnet add package Azure.Monitor.OpenTelemetry.Exporter
//
using Azure.Monitor.OpenTelemetry.Exporter;
//using OpenTelemetry;  
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using System.Diagnostics;
using System.Diagnostics.Metrics;

var builder = WebApplication.CreateBuilder(args);

// Configure OpenTelemetry with a resource (service name)
var resourceBuilder = ResourceBuilder.CreateDefault()
    .AddService(serviceName: builder.Environment.ApplicationName)
    .AddTelemetrySdk();

// Create a meter for custom metrics
var meter = new Meter("MinimalApiSample.Metrics");
var metricsCounter = meter.CreateCounter<int>("MetricsEndpointAccessCount");

// Add OpenTelemetry Tracing and Metrics for Azure Application Insights
builder.Services.AddOpenTelemetry()
    //.ConfigureResource(resourceBuilder) /**** This is the only compile time issue
    .ConfigureResource(rb=>rb =  resourceBuilder)
    .WithTracing(tracerProviderBuilder =>
    {
       tracerProviderBuilder
           .AddSource("MinimalApiSample")
           .AddAspNetCoreInstrumentation(options =>
           {
              options.RecordException = true;
           })
           .AddHttpClientInstrumentation()
           .AddAzureMonitorTraceExporter(options =>
           {
              options.ConnectionString = builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"];
           });
    })
    .WithMetrics(metricsProviderBuilder =>
    {
       metricsProviderBuilder
           .AddAspNetCoreInstrumentation()
           .AddHttpClientInstrumentation()
           .AddMeter("MinimalApiSample.Metrics") // Add your custom meter
           .AddAzureMonitorMetricExporter(options =>
           {
              options.ConnectionString = builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"];
           });
    });

var app = builder.Build();

// Example of a custom activity for tracing
var activitySource = new ActivitySource("MinimalApiSample");

app.MapGet("/", () =>
{
   using var activity = activitySource.StartActivity("RootEndpoint");
   activity?.SetTag("custom.tag", "Hello, Azure Application Insights!");
   return Results.Ok("Hello, Azure Application Insights!");
});

app.MapGet("/metrics", () =>
{
   // Increment custom metric on each access
   metricsCounter.Add(1);
   return Results.Ok("Metrics and traces are being sent to Azure Application Insights.");
});

app.Run();

I by pleasantly surprised by suggestion of a counter for each endpoint which was my original intent.

Couldn’t think of a better name “scirtem” is “metrics” backwards. The way Meter and CreateCount are global would not be a good idea in a more complex system but this is fine for a hacky PoC.

//dotnet add package OpenTelemetry
//dotnet add package OpenTelemetry.Extensions.Hosting
//dotnet add package OpenTelemetry.Instrumentation.AspNetCore
//dotnet add package OpenTelemetry.Instrumentation.Http
//dotnet add package Azure.Monitor.OpenTelemetry.Exporter
//
using Azure.Monitor.OpenTelemetry.Exporter;
//using OpenTelemetry;  //***** Unnecessary with OpenTelemetry.Extensions.Hosting
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using System.Diagnostics;
using System.Diagnostics.Metrics;

var builder = WebApplication.CreateBuilder(args);

// Configure OpenTelemetry with a resource (service name)
var resourceBuilder = ResourceBuilder.CreateDefault()
    .AddService(serviceName: builder.Environment.ApplicationName)
    .AddTelemetrySdk();

// Create a meter for custom metrics
var meter = new Meter("MinimalApiSample.Metrics");
var metricsCounter = meter.CreateCounter<int>("MetricsEndpointAccessCount");
var scirtemCounter = meter.CreateCounter<int>("ScirtemEndpointAccessCount");

// Add OpenTelemetry Tracing and Metrics for Azure Application Insights
builder.Services.AddOpenTelemetry()
    //.ConfigureResource(resourceBuilder) /**** This is the only compile time issue
    .ConfigureResource(rb=>rb =  resourceBuilder)
    .WithTracing(tracerProviderBuilder =>
    {
       tracerProviderBuilder
           .AddSource("MinimalApiSample")
           .AddAspNetCoreInstrumentation(options =>
           {
              options.RecordException = true;
           })
           .AddHttpClientInstrumentation()
           .AddAzureMonitorTraceExporter(options =>
           {
              options.ConnectionString = builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"];
           });
    })
    .WithMetrics(metricsProviderBuilder =>
    {
       metricsProviderBuilder
           .AddAspNetCoreInstrumentation()
           .AddHttpClientInstrumentation()
           .AddMeter("MinimalApiSample.Metrics") // Add your custom meter
           .AddAzureMonitorMetricExporter(options =>
           {
              options.ConnectionString = builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"];
           });
    });

var app = builder.Build();

// Example of a custom activity for tracing
var activitySource = new ActivitySource("MinimalApiSample");

app.MapGet("/", () =>
{
   using var activity = activitySource.StartActivity("RootEndpoint");
   activity?.SetTag("custom.tag", "Hello, Azure Application Insights!");
   return Results.Ok("Hello, Azure Application Insights!");
});

app.MapGet("/metrics", () =>
{
   // Increment custom metric on each access
   metricsCounter.Add(1);
   return Results.Ok("Metrics and traces are being sent to Azure Application Insights.");
});

app.MapGet("/scirtem", () =>
{
   scirtemCounter.Add(1);
   return Results.Ok("Scirtem endpoint accessed.");
});

app.Run();

Using Application Insights metrics the MetricsEndPointAccesCount, and ScirtemEndPointAccesCount, plots to show the OLTP telemetry emitted by the application.

Mistral generated the code for the endpoint latency histogram without any prompting.

//dotnet add package OpenTelemetry
//dotnet add package OpenTelemetry.Extensions.Hosting
//dotnet add package OpenTelemetry.Instrumentation.AspNetCore
//dotnet add package OpenTelemetry.Instrumentation.Http
//dotnet add package Azure.Monitor.OpenTelemetry.Exporter
//
using Azure.Monitor.OpenTelemetry.Exporter;
//using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using System.Diagnostics;
using System.Diagnostics.Metrics;

var builder = WebApplication.CreateBuilder(args);

// Configure OpenTelemetry with a resource (service name)
var resourceBuilder = ResourceBuilder.CreateDefault()
    .AddService(serviceName: builder.Environment.ApplicationName)
    .AddTelemetrySdk();

// Create a meter for custom metrics
var meter = new Meter("MinimalApiSample.Metrics");
var metricsCounter = meter.CreateCounter<int>("MetricsEndpointAccessCount");
var scirtemCounter = meter.CreateCounter<int>("ScirtemEndpointAccessCount");
var histogram = meter.CreateHistogram<double>("HistogramEndpointLatencyMs");

// Add OpenTelemetry Tracing and Metrics for Azure Application Insights
builder.Services.AddOpenTelemetry()
    //.ConfigureResource(resourceBuilder) /**** This is the only compile time issue
    .ConfigureResource(rb=>rb =  resourceBuilder)
    .WithTracing(tracerProviderBuilder =>
    {
       tracerProviderBuilder
           .AddSource("MinimalApiSample")
           .AddAspNetCoreInstrumentation(options =>
           {
              options.RecordException = true;
           })
           .AddHttpClientInstrumentation()
           .AddAzureMonitorTraceExporter(options =>
           {
              options.ConnectionString = builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"];
           });
    })
    .WithMetrics(metricsProviderBuilder =>
    {
       metricsProviderBuilder
           .AddAspNetCoreInstrumentation()
           .AddHttpClientInstrumentation()
           .AddMeter("MinimalApiSample.Metrics") // Register your custom meter
           .AddAzureMonitorMetricExporter(options =>
           {
              options.ConnectionString = builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"];
           });
    });

var app = builder.Build();

// Example of a custom activity for tracing
var activitySource = new ActivitySource("MinimalApiSample");

app.MapGet("/", () =>
{
   using var activity = activitySource.StartActivity("RootEndpoint");
   activity?.SetTag("custom.tag", "Hello, Azure Application Insights!");
   return Results.Ok("Hello, Azure Application Insights!");
});

app.MapGet("/metrics", () =>
{
   metricsCounter.Add(1);
   return Results.Ok("Metrics endpoint accessed.");
});

app.MapGet("/scirtem", () =>
{
   scirtemCounter.Add(1);
   return Results.Ok("Scirtem endpoint accessed.");
});

app.MapGet("/histogram", async () =>
{
   // Simulate some work
   var startTime = Stopwatch.GetTimestamp();
   await Task.Delay(Random.Shared.Next(50, 200)); // Random delay between 50-200ms
   var endTime = Stopwatch.GetTimestamp();

   // Calculate latency in milliseconds
   var latencyMs = (endTime - startTime) * 1000.0 / Stopwatch.Frequency;
   histogram.Record(latencyMs);

   return Results.Ok($"Histogram endpoint accessed. Latency: {latencyMs:F2}ms");
});

app.Run();

Using Application Insights metrics the OpenTelemetry.HistogramEndpointLatencyMs plot to show the OLTP telemetry emitted by the application.

Even with my relatively trivial OTLP learning applications, Mistral consistently produced clean and usable code with my simple prompts (maybe, I have got better and prompting). The generated code was straightforward, required only minor fixes, and avoided much of the over-complexity I’d seen in earlier experiments with other tools (looking at you mid/late 2025 Copilot). For my simple OTLP observability learning scenarios, that translated into faster iteration and less time spent refactoring and debugging generated code.

Message Transformation with cached transform binaries

The second prototype of “transforming” telemetry data used C# code complied and binary cached on demand. The HiveMQClient based application subscribes to topics (devices publishing environmental measurements) and then republishes them to multiple topics.

public class messageTransformer : IMessageTransformer
{
   public MQTT5PublishMessage[] Transform(MQTT5PublishMessage message)
   {
      if (message.Payload is null)
      {
         return [];
      }

      var payload = Encoding.UTF8.GetString(message.Payload);

      // Simple transformations: convert to both upper and lower case
      var toLower = new MQTT5PublishMessage
      {
         Topic = message.Topic,
         Payload = Encoding.UTF8.GetBytes(payload.ToLower()),
         QoS = QualityOfService.AtLeastOnceDelivery
      };

      var toUpper = new MQTT5PublishMessage
      {
         Topic = message.Topic,
         Payload = Encoding.UTF8.GetBytes(payload.ToUpper()),
         QoS = QualityOfService.AtLeastOnceDelivery
      };

      return [toLower, toUpper];
   }
}

The sample C# code (LowerUpper.cs) implements the IMessageTransformer interface and republishes both lower and upper case versions of the message.

Once the transformer had been loaded then compiled there was no noticeable difference between the application, loaded from constant string, and loaded from external file versions.

private static void OnMessageReceived(object? sender, HiveMQtt.Client.Events.OnMessageReceivedEventArgs e)
{
   HiveMQClient client = (HiveMQClient)sender!;

   Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} HiveMQ.receive start");
   Console.WriteLine($" Topic:{e.PublishMessage.Topic} QoS:{e.PublishMessage.QoS} Payload:{e.PublishMessage.PayloadAsString}");

   Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} HiveMQ.Publish start");
   foreach (string topic in _applicationSettings.PublishTopics.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
   {
      e.PublishMessage.Topic = string.Format(topic, _applicationSettings.ClientId);

      var transformer = _scriptEngine.GetTransformer();

      if (transformer is null)
      {
         Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} Transformer is null");
         return;
      }

      var transformedMessages = transformer.Transform(e.PublishMessage);
      if (transformedMessages is null)
      {
         Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} Transformer returned null");
         return;
      }

      if (transformedMessages.Length == 0)
      {
         Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} Transformer returned no messages");
         return;
      }

      foreach (MQTT5PublishMessage message in transformer.Transform(e.PublishMessage))
      {
         if (message is null)
         {
            Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} Transformer message is null");

            continue;
         }

         try
         {
            Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} Topic:{e.PublishMessage.Topic} HiveMQ Publish start ");

            var resultPublish = client.PublishAsync(message).GetAwaiter().GetResult();

            Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} Published:{resultPublish.QoS1ReasonCode} {resultPublish.QoS2ReasonCode}");
         }
         catch (Exception ex)
         {
            Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} HiveMQ Publish exception {ex.Message}");
         }
      }
   }
   Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} HiveMQ.Receive finish");
}

I used a local instance of NanoMQ: An Ultra-lightweight MQTT Broker for IoT Edge for testing

The MQTTX application subscribed to topics that devices (XiaoTandHandCO2A, XiaoTandHandCO2B etc.) and the simulated bridge (in my case DESKTOP-EN0QGL0) published.

Message Transformation in code with HiveMQ Client

The first prototype of “transforming” telemetry data used C# code complied with the application. The HiveMQClient based application subscribes to topics (devices publishing environmental measurements) and then republishes them to multiple topics.

private static void OnMessageReceived(object? sender, HiveMQtt.Client.Events.OnMessageReceivedEventArgs e)
{
   HiveMQClient client = (HiveMQClient)sender!;

   Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} HiveMQ.receive start");
   Console.WriteLine($" Topic:{e.PublishMessage.Topic} QoS:{e.PublishMessage.QoS} Payload:{e.PublishMessage.PayloadAsString}");

   Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} HiveMQ.Publish start");
   foreach (string topic in _applicationSettings.PublishTopics.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
   {
      e.PublishMessage.Topic = string.Format(topic, _applicationSettings.ClientId);

      Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} Topic:{e.PublishMessage.Topic} HiveMQ Publish start ");
      var resultPublish = client.PublishAsync(e.PublishMessage).GetAwaiter().GetResult();
      Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} Published:{resultPublish.QoS1ReasonCode} {resultPublish.QoS2ReasonCode}");
   }
   Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} HiveMQ.Receive finish");
}

The MQTTX application subscribed to topics that devices (XiaoTandHandCO2A, XiaoTandHandCO2B etc.) and the simulated bridge (DESKTOP-EN0QGL0) published.

The second prototype “transforms” the telemetry message payloads with C# code that is compiled (with CSScript) as the application starts. The application subscribes to the topics which devices publish (environmental measurements), transforms the payloads, and then republishes the transformed messages to “bridge” topics

private static void OnMessageReceived(object? sender, HiveMQtt.Client.Events.OnMessageReceivedEventArgs e)
{
   HiveMQClient client = (HiveMQClient)sender!;

   Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} HiveMQ.receive start");
   Console.WriteLine($" Topic:{e.PublishMessage.Topic} QoS:{e.PublishMessage.QoS} Payload:{e.PublishMessage.PayloadAsString}");

   Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} HiveMQ.Publish start");
   foreach (string topic in _applicationSettings.PublishTopics.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
   {
      e.PublishMessage.Topic = string.Format(topic, _applicationSettings.ClientId);

      foreach (MQTT5PublishMessage message in _evaluator.Transform(e.PublishMessage))
      {
         Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} Topic:{e.PublishMessage.Topic} HiveMQ Publish start ");
         var resultPublish = client.PublishAsync(message).GetAwaiter().GetResult();
         Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} Published:{resultPublish.QoS1ReasonCode} {resultPublish.QoS2ReasonCode}");
      }
   }
   Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} HiveMQ.Receive finish");
}

// This code is compiled as the application starts up, it implements the IMessageTransformer interface
const string sampleTransformCode = @"
using System.Text;
using HiveMQtt.MQTT5.Types;

public class messageTransformer : devMobile.IoT.MqttTransformer.CSScriptLoopback.IMessageTransformer
{
   public MQTT5PublishMessage[] Transform(MQTT5PublishMessage message)
   {
      // Example: echo the payload to a new topic
      var payload = Encoding.UTF8.GetString(message.Payload);


      // Simple transformations: convert to uppercase or lowercase
      var toLower = new MQTT5PublishMessage
      {
         Topic = message.Topic,
         Payload = Encoding.UTF8.GetBytes(payload.ToLower()),
         QoS = QualityOfService.AtLeastOnceDelivery
      };

      var toUpper = new MQTT5PublishMessage
      {
         Topic = message.Topic,
         Payload = Encoding.UTF8.GetBytes(payload.ToUpper()),
         QoS = QualityOfService.AtLeastOnceDelivery
      };

      return new[] { toLower, toUpper };
   }
}";

The sample C# code implements the IMessageTransformer interface and republishes lower and upper case versions of the message.

public interface IMessageTransformer
{
   public MQTT5PublishMessage[] Transform(MQTT5PublishMessage mqttPublishMessage);
}
...
  _evaluator = CSScript.Evaluator.LoadCode<IMessageTransformer>(sampleTransformCode);

The SwarmSpace, and Myriota gateways both use an interface based approach to process uplink and downlink messages. Future versions will include support for isolating processing so that a rogue script can’t crash the application or reference unapproved assemblies

NanoMQ on Windows fail

After hours of fail trying to get nanoMQ TCP bridge running on my Windows11 development system it was time to walk away. I ran nanoMQ with different log levels but “nng_dialer_create failed 9” was the initial error message displayed.

The setup looked good…

bridges.mqtt.MyBridgeDeviceID {
    ## Azure Event Grid MQTT broker endpoint
  server = "tls+mqtt-tcp://xxxx.newzealandnorth-1.ts.eventgrid.azure.net:8883"
  proto_ver   = 5
  clientid    = "MyBridgeDeviceID"
  username    = "MyBridgeDeviceID"
  clean_start = true
  keepalive   = "60s"

  ## TLS client certificate authentication
  ssl = {
    # key_password = ""
    keyfile    = "certificates/MyBridgeDeviceID.key"
    certfile   = "certificates/MyBridgeDeviceID.crt"
    cacertfile = "certificates/xxxx.crt"
  }
  ## ------------------------------------------------------------
  ## Topic forwarding (NanoMQ → Azure Event Grid)
  ## ------------------------------------------------------------
  ## These are the topics your device publishes locally.
  ## They will be forwarded upstream to Event Grid.
  ##
  forwards = [xxxx]

  ## ------------------------------------------------------------
  ## Topic subscription (Azure Event Grid → NanoMQ)
  ## ------------------------------------------------------------
  ## This is the topic your device subscribes to from Event Grid.
  subscription = [xxxx]
}

Turns out the nanomq-windows-x86_64 version is not built with Transport Layer Security(TLS) or Dashboard support enabled and If I had started with my Seeedstudio EdgeBox 200 the configuration would most probably have worked.

The management API did work, though I don’t understand why they didn’t use a more RESTfull approach e.g. using HTTP Status codes.

NanoMQ with a HiveMQ Client

Most of my applications have focused on telemetry but I had been thinking about local control for solutions that have to run disconnected. In “real-world” deployments connectivity to Azure EventGrid MQTT Broker isn’t 100% reliable (also delay and jitter issues) which are an issue for control at the edge.

The approach to “transforming” telemetry data into “commands” has to be reliable, supportable, testable, scalable and portable (different processor architectures and operating systems). There are several Edge MQTT brokers which meet most, or all of these criteria and this series of posts will use NanoMQ a Linux Foundation Edge project which can run on my development system reComputer Industrial J3011- Fanless Edge AI, and Seeedstudio EdgeBox 200 devices.

The HiveMQClient application could publish and subscribe to topics

The MQTTX application could also publish and subscribe to topics

The HiveMQClient application has no way to “gracefully” shutdown which was visible in the NanoMQ console.

I have cut corners, the support for secure connections to nanoMQ is very limited and this setup should only be used for basic proof of concepts

Building Edge AI with Github Copilot- Security Camera HTTP YoloSharp

When I started with the Security Camera HTTP code and added code to process the images with Ultralytics Object Detection model I found the order of the prompts could make a difference. My first attempt at adding YoloSharp to the SecurityCameraHttpClient application with Github Copilot didn’t go well and needed some “human intervention”. When I thought more about the order of the prompts the adding the same functionality went a lot better.

// Use a stream rather than loading image from a file
// Use YoloSharp to run an onnx Object Detection model on the image
// Make the YoloPredictor a class variable
// Save image if object with specified image class name detected
// Modify so objectDetected supports multiple image class names
// Modify code to make use of GPU configurable
// Make display of detections configurable in app settings
// Make saving of image configurable in app settings

internal class Program
{
   private static HttpClient _client;
   private static bool _isRetrievingImage = false;
   private static ApplicationSettings _applicationSettings;
   private static YoloPredictor _yoloPredictor;

   static void Main(string[] args)
   {
      Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss} SecurityCameraClient starting");
#if RELEASE
         Console.WriteLine("RELEASE");
#else
         Console.WriteLine("DEBUG");
#endif

      var configuration = new ConfigurationBuilder()
            .AddJsonFile("appsettings.json", false, true)
            .AddUserSecrets<Program>()
            .Build();

      _applicationSettings = configuration.GetSection("ApplicationSettings").Get<ApplicationSettings>();

      // Initialize YoloPredictor with GPU configuration
      _yoloPredictor = new YoloPredictor(_applicationSettings.OnnxModelPath, new YoloPredictorOptions()
      {
         UseCuda = _applicationSettings.UseCuda, // Configurable GPU usage
      });

      using (HttpClientHandler handler = new HttpClientHandler { Credentials = new NetworkCredential(_applicationSettings.Username, _applicationSettings.Password) })
      using (_client = new HttpClient(handler))
      using (var timer = new Timer(async _ => await RetrieveImageAsync(), null, _applicationSettings.TimerDue, _applicationSettings.TimerPeriod))
      {
         Console.WriteLine("Press any key to exit...");
         Console.ReadKey();
      }
   }

      private static async Task RetrieveImageAsync()
      {
         if (_isRetrievingImage) return;

         _isRetrievingImage = true;
         try
         {
            Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss.fff} SecurityCameraClient download starting");

            HttpResponseMessage response = await _client.GetAsync(_applicationSettings.CameraUrl);
            response.EnsureSuccessStatusCode();

            using (Stream imageStream = await response.Content.ReadAsStreamAsync())
            {
               var detections = _yoloPredictor.Detect(imageStream);
               bool objectDetected = false;

               foreach (var detection in detections)
               {
                  if (_applicationSettings.LogDetections) // Check if logging detections is enabled
                  {
                     Console.WriteLine($"Detected {detection.Name.Name} with confidence {detection.Confidence}");
                  }

                  if (_applicationSettings.ClassNames.Contains(detection.Name.Name))
                  {
                     objectDetected = true;
                  }
               }

               if (objectDetected && _applicationSettings.SaveImage) // Check if saving images is enabled
               {
                  string savePath = string.Format(_applicationSettings.SavePath, DateTime.UtcNow);
                  using (FileStream fileStream = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.None))
                  {
                     imageStream.Position = 0;
                     await imageStream.CopyToAsync(fileStream);
                  }
               }
            }

            Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss.fff} SecurityCameraClient download done");
         }
         catch (Exception ex)
         {
            Console.WriteLine($"An error occurred: {ex.Message}");
         }
         finally
         {
            _isRetrievingImage = false;
         }
      }
}

public class ApplicationSettings
{
   public string CameraUrl { get; set; } = "";
   public string SavePath { get; set; } = "";
   public string Username { get; set; } = "";
   public string Password { get; set; } = "";
   public TimeSpan TimerDue { get; set; } = TimeSpan.Zero;
   public TimeSpan TimerPeriod { get; set; } = TimeSpan.Zero;
   public string OnnxModelPath { get; set; } = "";
   public bool UseCuda { get; set; } = false; // Configurable GPU usage
   public List<string> ClassNames { get; set; } //= new List<string>();
   public bool LogDetections { get; set; } = false; // Configurable logging of detections 
   public bool SaveImage { get; set; } = false; // Configurable saving of images
   }
}

The interactions Visual Studio IntelliSense with the GitHub Copilot prompts was interesting.

I wonder if this is because Visual Studio Intellisense has local context, whereas Github Copilot has “cloud” context.

It took a couple of failed attempts to find the best order, which I think would reduce over time.

The Copilot generated code in this post is not suitable for production

RTSP Camera rosenbjerg.FFMpegCore GDI Error

While working on my SecurityCameraRTSPClientFFMpegCore project I noticed that every so often after opening the Realtime Streaming Protocol(RTSP) connection with my HiLook IPCT250H Security Camera there was a “Paremeter is not valid” or “A generic error occurred in GDI+.” exception and sometimes the image was corrupted.

My test harness code was “inspired” by the Continuous Snapshots on Live Stream #280 sample

sing (var ms = new MemoryStream())
{
    await FFMpegArguments
        .FromUrlInput(new Uri("udp://192.168.2.12:9000"))
        .OutputToPipe(new StreamPipeSink(ms), options => options
            .ForceFormat("rawvideo")
            .WithVideoCodec(VideoCodec.Png)
            .Resize(new Size(Config.JpgWidthLarge, Config.JpgHeightLarge))
            .WithCustomArgument("-vf fps=1 -update 1")
        )
        .NotifyOnProgress(o => 
        {
            try
            {
                if (ms.Length > 0)
                {
                    ms.Position = 0;
                    using (var bitmap = new Bitmap(ms))
                    {
                        // Modify bitmap here

                        // Save the bitmap
                        bitmap.Save("test.png");
                    }

                    ms.SetLength(0);
                }
            }
            catch { }
        })
        .ProcessAsynchronously();
}

My implementation is slightly different because I caught then displayed any exceptions generated converting the image stream to a bitmap or saving it.

using (var ms = new MemoryStream())
{
   await FFMpegArguments
         .FromUrlInput(new Uri(_applicationSettings.CameraUrl))
         .OutputToPipe(new StreamPipeSink(ms), options => options
         .ForceFormat("mpeg1video")
         //.ForceFormat("rawvideo")
         .WithCustomArgument("-rtsp_transport tcp")
         .WithFramerate(10)
         .WithVideoCodec(VideoCodec.Png)
         //.Resize(1024, 1024)
         //.ForceFormat("image2pipe")
         //.Resize(new Size(Config.JpgWidthLarge, Config.JpgHeightLarge))
         //.Resize(new Size(Config.JpgWidthLarge, Config.JpgHeightLarge))
         //.WithCustomArgument("-vf fps=1 -update 1")
         //.WithCustomArgument("-vf fps=5 -update 1")
         //.WithSpeedPreset( Speed.)
         //.UsingMultithreading()
         //.UsingThreads()
         //.WithVideoFilters(filter => filter.Scale(640, 480))
         //.UsingShortest()
         //.WithFastStart()
         )
         .NotifyOnProgress(o =>
         {
            try
            {
               if (ms.Length > 0)
               {
                  ms.Position = 0;

                  string outputPath = Path.Combine(_applicationSettings.SavePath, string.Format(_applicationSettings.FrameFileNameFormat, DateTime.UtcNow ));

                  using (var bitmap = new Bitmap(ms))
                  {
                     // Save the bitmap
                     bitmap.Save(outputPath);
                  }

                  ms.SetLength(0);
               }
            }
            catch (Exception ex)
            {
               Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss.fff} {ex.Message}");
            }
         })
         .ProcessAsynchronously();
}

I have created a Continuous Snapshots on Live Stream Memory stream contains invalid bitmap image #562 to track the issue.

One odd thing that I noticed when scrolling “back and forth” through the images around when there was exception was that the date and time on the top left of the image was broken.

I wonder if the image was “broken” in some subtle way and FFMpegCore is handling this differently to the other libraries I’m trialing.

RTSP Camera RabbitOM.Streaming

The RTSPCameraNagerVideoStream library had significant latency which wasn’t good as I wanted to trigger the processing of images from the Real-time Streaming Protocol(RTSP) on my Seeedstudio J3011 Industrial device by strobing one of the digital inputs and combine streamed images with timestamped static ones.

HiLook IPCT250H Camera configuration

To get a Moving Picture Experts Group(MPEG) stream I had to change the camera channel rather than use than H.264+ video Encoding

RtspCameraUrl”: “rtsp://10.0.0.19/ISAPI/Streaming/channels/102”

The KSAH-42.RabbitOM library looked worth testing so I built a test harness inspired by RabbitOM.Streaming.Tests.ConsoleApp.

client.PacketReceived += (sender, e) =>
{
   var interleavedPacket = e.Packet as RtspInterleavedPacket;

   if (interleavedPacket != null && interleavedPacket.Channel > 0)
   {
      // In most of case, avoid this packet
      Console.ForegroundColor = ConsoleColor.DarkCyan;
      Console.WriteLine("Skipping some data : size {0}", e.Packet.Data.Length);
      return;
   }

   Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss.fff} New image received, bytes:{e.Packet.Data.Length}");

   File.WriteAllBytes(Path.Combine(_applicationSettings.SavePath, string.Format(_applicationSettings.FrameFileNameFormat, DateTime.UtcNow)), e.Packet.Data);
};

When I ran my test harness the number of images didn’t match the frame rate configured in the camera

The format of the images was corrupted, and I couldn’t open them

It looked like I was writing RTSP packets to the disk rather than Joint Photographic Experts Group(JPEG) images from the MPEG stream.

There was another sample application RabbitOM.Streaming.Tests.Mjpeg which displayed JPEG images. After looking at the code I figured out I need to use the RtpFrameBuilder class to assemble the RTSP packets into frames.

private static readonly RtpFrameBuilder _frameBuilder = new JpegFrameBuilder();
...
_frameBuilder.FrameReceived += OnFrameReceived;
...
client.PacketReceived += (sender, e) =>
{
   var interleavedPacket = e.Packet as RtspInterleavedPacket;

   if (interleavedPacket != null && interleavedPacket.Channel > 0)
   {
      // In most of case, avoid this packet
      Console.ForegroundColor = ConsoleColor.DarkCyan;
      Console.WriteLine("Skipping some data : size {0}", e.Packet.Data.Length);
      return;
   }

   _frameBuilder.Write(interleavedPacket.Data); 
};
private static void OnFrameReceived(object sender, RtpFrameReceivedEventArgs e)
{
   Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss.fff} New image received, bytes:{e.Frame.Data.Length}");

   File.WriteAllBytes(Path.Combine(_applicationSettings.SavePath, string.Format(_applicationSettings.FrameFileNameFormat, DateTime.UtcNow)), e.Frame.Data);
}

With the modified code the image size looked roughly the same as the SecurityCameraHttpClient images

The format of the images was good, and I could open them

Looks like KSAH-42.RabbitOM might be a good choice as it doesn’t have any external dependencies and the latency is minimal.

Building Edge AI with Github Copilot- Security Camera HTTP

I use GitHub Copilot with Visual Studio 2020 and I wanted to see if I could write a simple application only using “Ask Copilot”. Retrieving a static image from a security camera (like my HiLook IPC-T250H) felt like a reasonable challenge.

please write a console application that uses an HttpClient to retrieve an image from a security camera

static async Task Main(string[] args)
{
   string cameraUrl = "http://your-security-camera-url/image.jpg";
   string savePath = "downloaded_image.jpg";

   using (HttpClient client = new HttpClient())
   {
      try
      {
         HttpResponseMessage response = await client.GetAsync(cameraUrl);
         response.EnsureSuccessStatusCode();

         byte[] imageBytes = await response.Content.ReadAsByteArrayAsync();
         await File.WriteAllBytesAsync(savePath, imageBytes);

         Console.WriteLine("Image downloaded successfully.");
      }
      catch (Exception ex)
      {
         Console.WriteLine($"An error occurred: {ex.Message}");
      }
   }
}
}

The Github Copilot generated code compiled first time and looked reasonable for a Proof of Concept (PoC). After changing the “cameraUrl” the program could retrieve an image from one of my cameras when I disabled authentication.

please write a console application that uses an httpclient to retrieve an image from a security camera. The security camera needs a username and password

static async Task Main(string[] args)
{
   string cameraUrl = "http://your-security-camera-url/image";
   string username = "your-username";
   string password = "your-password";

   using (HttpClient client = new HttpClient())
   {
      var byteArray = new System.Text.ASCIIEncoding().GetBytes($"{username}:{password}");
      client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray));

      HttpResponseMessage response = await client.GetAsync(cameraUrl);
      response.EnsureSuccessStatusCode();

      byte[] imageBytes = await response.Content.ReadAsByteArrayAsync();
      File.WriteAllBytes("camera_image.jpg", imageBytes);

      Console.WriteLine("Image retrieved and saved as camera_image.jpg");
   }

Github Copilot had taken my request literally and implemented Hypertext Transfer Protocol (HTTP/1.1): Authentication (RFC7235). My HiLook IPC-T250H only supports Digest Authentication(RFC2617) so I tried

please modify the security code to support Digest authentication

static async Task Main(string[] args)
{
   string cameraUrl = "http://your-security-camera-url/image";
   string username = "your-username";
   string password = "your-password";

   using (HttpClient client = new HttpClient())
   {
      HttpResponseMessage response = await client.GetAsync(cameraUrl);

      if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
      {
         var wwwAuthenticateHeader = response.Headers.WwwAuthenticate.FirstOrDefault();
         if (wwwAuthenticateHeader != null && wwwAuthenticateHeader.Scheme == "Digest")
         {
            var digestHeader = wwwAuthenticateHeader.Parameter;
            var digestResponse = CreateDigestHeader(digestHeader, cameraUrl, username, password, "GET");

            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Digest", digestResponse);

            response = await client.GetAsync(cameraUrl);
            response.EnsureSuccessStatusCode();

            byte[] imageBytes = await response.Content.ReadAsByteArrayAsync();
            File.WriteAllBytes("camera_image.jpg", imageBytes);

            Console.WriteLine("Image retrieved and saved as camera_image.jpg");
         }
      }
   }
}

private static string CreateDigestHeader(string digestHeader, string url, string username, string password, string method)
{
   var headerValues = digestHeader.Split(',').Select(v => v.Trim().Split('=')).ToDictionary(v => v[0], v => v[1].Trim('"'));

   string ha1 = CalculateMD5Hash($"{username}:{headerValues["realm"]}:{password}");
   string ha2 = CalculateMD5Hash($"{method}:{url}");
   string response = CalculateMD5Hash($"{ha1}:{headerValues["nonce"]}:{headerValues["nc"]}:{headerValues["cnonce"]}:{headerValues["qop"]}:{ha2}");

   return $"username=\"{username}\", realm=\"{headerValues["realm"]}\", nonce=\"{headerValues["nonce"]}\", uri=\"{url}\", qop={headerValues["qop"]}, nc={headerValues["nc"]}, cnonce=\"{headerValues["cnonce"]}\", response=\"{response}\", opaque=\"{headerValues["opaque"]}\"";
}

private static string CalculateMD5Hash(string input)
{
   using (MD5 md5 = MD5.Create())
   {
      byte[] inputBytes = Encoding.ASCII.GetBytes(input);
      byte[] hashBytes = md5.ComputeHash(inputBytes);
      return BitConverter.ToString(hashBytes).Replace("-", "").ToLower();
   }
}

Github Copilot had taken my request literally and implemented Digest Authentication(RFC2617) so I did some “research”. The Copilot desktop application suggested using NetworkCredential which supports Basic, and digest (also NTLM, and Kerberos) authentication.

modify the code to use a NetworkCredential to authenticate with the security camera

using System.Net;

namespace SecurityCameraClient
{
   internal class Program
   {
      static async Task Main(string[] args)
      {
         string cameraUrl = "http://your-security-camera-url/image.jpg";
         string savePath = "downloaded_image.jpg";
         string username = "your-username";
         string password = "your-password";

         using (HttpClientHandler handler = new HttpClientHandler { Credentials = new NetworkCredential(username, password) })
         using (HttpClient client = new HttpClient(handler))
         {
            try
            {
               HttpResponseMessage response = await client.GetAsync(cameraUrl);
               response.EnsureSuccessStatusCode();

               byte[] imageBytes = await response.Content.ReadAsByteArrayAsync();
               await File.WriteAllBytesAsync(savePath, imageBytes);

               Console.WriteLine("Image downloaded successfully.");
            }
            catch (Exception ex)
            {
               Console.WriteLine($"An error occurred: {ex.Message}");
            }
         }
      }
   }
}

The Github Copilot generated code compiled and after changing the “cameraUrl”, “username” and “password” the program could retrieve an image from each of my cameras.

I wanted the application to retrieve images regularly, but the first Github Copilot generated code was pretty ugly (it used a System.Timers.Timer). After “consulting” the Copilot desktop application

modify the code to use a System.Threading Timer so an image is retrieved every 1000mSec

using System.Net;

namespace SecurityCameraClient
{
   internal class Program
   {
      private static Timer _timer;
      private static HttpClient _client;
      private static string _cameraUrl = "http://your-security-camera-url/image.jpg";
      string savePath = "downloaded_image.jpg";

      static void Main(string[] args)
      {
         string username = "your-username";
         string password = "your-password";

         HttpClientHandler handler = new HttpClientHandler { Credentials = new NetworkCredential(username, password) };
         _client = new HttpClient(handler, disposeHandler: true);

         _timer = new Timer(async _ => await RetrieveImageAsync(), null, 0, 1000);

         Console.WriteLine("Press any key to exit...");
         Console.ReadKey();

         _timer.Dispose();
         _client.Dispose();
      }

      private static async Task RetrieveImageAsync()
      {
         try
         {
            HttpResponseMessage response = await _client.GetAsync(_cameraUrl);
            response.EnsureSuccessStatusCode();

            byte[] imageBytes = await response.Content.ReadAsByteArrayAsync();
            await File.WriteAllBytesAsync(_savePath, imageBytes);

            Console.WriteLine("Image downloaded successfully.");
         }
         catch (Exception ex)
         {
            Console.WriteLine($"An error occurred: {ex.Message}");
         }
      }
   }
}

By this stage I was an elite “prompt engineer” and after a couple of revisions of my Github Copilot prompt to released unmanaged resources (the initial version used .Dispose and was a bit ugly).

modify the code to dispose of the HttpClientHandler, HttpClient and Timer with “using” statements

The application had a reentrancy issue when retrieving an image from a camera took too long

modify the code to stop RetrieveImageAsync getting called while an image is already being retrieved

I then decided to try a “cosmetic” change

modify the code _timer does not have to be class level variable

The savePath was a constant and I wanted to store a series of images

modify the code to use String.Format to generate the savepath

For the final version I modified the program adding a Console.Writeline to display the build type, retrieving the SavePath, dueTime, and period from the appsettings.json. The Microsoft.Configuration.UserSecrets configuration source was used for the CameraUrl, UserName, and Password.

using System.Net;

using Microsoft.Extensions.Configuration;

namespace SecurityCameraClient
{
   internal class Program
   {
      private static HttpClient _client;
      private static bool _isRetrievingImage = false;
      private static ApplicationSettings _applicationSettings;

      static void Main(string[] args)
      {
         Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss} SecurityCameraClient starting");
#if RELEASE
         Console.WriteLine("RELEASE");
#else
         Console.WriteLine("DEBUG");
#endif

         var configuration = new ConfigurationBuilder()
              .AddJsonFile("appsettings.json", false, true)
         .AddUserSecrets<Program>()
         .Build();

         _applicationSettings = configuration.GetSection("ApplicationSettings").Get<ApplicationSettings>();

         using (HttpClientHandler handler = new HttpClientHandler { Credentials = new NetworkCredential(_applicationSettings.Username, _applicationSettings.Password) })
         using (_client = new HttpClient(handler))
         using (var timer = new Timer(async _ => await RetrieveImageAsync(), null, _applicationSettings.TimerDue, _applicationSettings.TimerPeriod))
         {
            Console.WriteLine("Press any key to exit...");
            Console.ReadKey();
         }
      }

      private static async Task RetrieveImageAsync()
      {
         if (_isRetrievingImage) return;

         _isRetrievingImage = true;
         try
         {
            HttpResponseMessage response = await _client.GetAsync(_applicationSettings.CameraUrl);
            response.EnsureSuccessStatusCode();

            byte[] imageBytes = await response.Content.ReadAsByteArrayAsync();
            string savePath = string.Format(_applicationSettings.SavePath, DateTime.UtcNow);
            await File.WriteAllBytesAsync(savePath, imageBytes);

            Console.WriteLine("Image downloaded successfully.");
         }
         catch (Exception ex)
         {
            Console.WriteLine($"An error occurred: {ex.Message}");
         }
         finally
         {
            _isRetrievingImage = false;
         }
      }
   }

   public class ApplicationSettings
   {
      public string CameraUrl { get; set; } = "";

      public string SavePath { get; set; } = "";

      public string Username { get; set; } = "";

      public string Password { get; set; } = "";

      public TimeSpan TimerDue { get; set; } = TimeSpan.Zero;

      public TimeSpan TimerPeriod { get; set; } = TimeSpan.Zero;
   }
}

Overall, my Github Copilot experience was pretty good, and got better as my “prompt engineering” improved.

The Github Copilot “decision” to implement Hypertext Transfer Protocol (HTTP/1.1): Authentication (RFC7235) and Digest Authentication(RFC2617) was “sub optimal”

IoT and AI Building Edge AI with AI- Introduction

Introduction

Recently, there have been mainstream media articles (Forbes, Wired-Video games , Wired-Copilot, BBC, The Guardian etc.), about how Artificial Intelligence (AI) could soon replace developers.

I have been using Github Copilot, mainly for “enhanced” Visual Studio IntelliSense, code snippets, and refactoring. I have also found Github Copilot generated code samples and more context aware searching of documentation really useful. Overall, the experience has been positive with limited hallucinations and my productivity has got better as my “prompt engineer” skills improved.

Initially, I started building “hobbyist” AI applications for the edge with an Azure Percept and for customer projects use Ultralytics Yolo, Azure Machine Learning, Azure Cognitive Services, ML.Net, Open Neural Network Exchange (ONNX), Nvidia Jeston Modules, Seeedstudio reComputer Industrial J3011 – Fanless Edge AI Devices etc.

After some discussion with my co-workers I decided to explore how much Github Copilot helped building a “non-trivial” computer vision Proof of Concept (PoC). In previous blog posts I have written about capturing still images from a Security Camera so figured that was a good place to start.

Even though I have been using Copilot for a while I still say please.

using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;

class Program
{
    static async Task Main(string[] args)
    {
        string cameraUrl = "http://your-security-camera-url/image.jpg"; // Replace with your security camera URL
        string savePath = "C:\\path\\to\\save\\image.jpg"; // Replace with the path where you want to save the image

        using (HttpClient client = new HttpClient())
        {
            try
            {
                HttpResponseMessage response = await client.GetAsync(cameraUrl);
                response.EnsureSuccessStatusCode();

                byte[] imageBytes = await response.Content.ReadAsByteArrayAsync();
                await File.WriteAllBytesAsync(savePath, imageBytes);

                Console.WriteLine("Image retrieved and saved successfully.");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"An error occurred: {ex.Message}");
            }
        }
    }
}

The generated code compiled first time, looked “ok” and worked with some of my security cameras when the cameraurl and save path were modified

I then modified the code so “sensitive” information was not stored in code or configuration files.

//using System;
//using System.IO;
//using System.Net.Http;
//using System.Reflection;
//using System.Threading.Tasks;

using Microsoft.Extensions.Configuration;

class Program
{
   static async Task Main(string[] args)
   {
      //string cameraUrl = "http://your-security-camera-url/image.jpg"; // Replace with your security camera URL
      //string savePath = "C:\\path\\to\\save\\image.jpg"; // Replace with the path where you want to save the image
      Model.ApplicationSettings applicationSettings;

   Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss} 0.2.CameraHTTPClient starting");
#if RELEASE
         Console.WriteLine("RELEASE");
#else
      Console.WriteLine("DEBUG");
#endif
      // load the app settings into configuration
      var configuration = new ConfigurationBuilder()
           .AddJsonFile("appsettings.json", false, true)
      .AddUserSecrets<Program>()
      .Build();

      applicationSettings = configuration.GetSection("ApplicationSettings").Get<Model.ApplicationSettings>();

      using (HttpClient client = new HttpClient())
      {
         try
         {
            HttpResponseMessage response = await client.GetAsync(applicationSettings.CameraUrl);
            response.EnsureSuccessStatusCode();

            byte[] imageBytes = await response.Content.ReadAsByteArrayAsync();
            await File.WriteAllBytesAsync(applicationSettings.SavePath, imageBytes);

            Console.WriteLine("Image retrieved and saved successfully.");
         }
         catch (Exception ex)
         {
            Console.WriteLine($"An error occurred: {ex.Message}");
         }
      }
   }
}

namespace Model
{
   public class ApplicationSettings
   {
      public string CameraUrl { get; set; } = "";

      public string SavePath { get; set; } = "Images";
   }
}

Several of my security cameras (e.g. HILook T250H) required client to authentication so I added NetworkCredential my Copilot prompt.

// please write c# console application that uses httpclient and an httpget to retrieve image from security camera with NetworkCredential security
//
// Sure! Here's a C# console application that uses HttpClient and HttpGet to retrieve an image from a security camera with NetworkCredential security: 
// In this example:
//
// Replace http://your-security-camera-url/image.jpg with the actual URL of your security camera's image.
//
// Replace C:\\path\\to\\save\\image.jpg with the path where you want to save the retrieved image.
//
// Replace your-username and your-password with your actual credentials.
//
// This application uses HttpClient with NetworkCredential to authenticate the request, retrieves the image as a byte array, and saves it to the specified path. If there's an error during the process, it will be caught and displayed in the console.
//
// Happy coding! 🚀
//using System;
//using System.IO;
using System.Net;
//using System.Net.Http;
//using System.Threading.Tasks;

using Microsoft.Extensions.Configuration;

class Program
{
   static async Task Main(string[] args)
   {
      //string cameraUrl = "http://your-security-camera-url/image.jpg"; // Replace with your security camera URL
      //string savePath = "C:\\path\\to\\save\\image.jpg"; // Replace with the path where you want to save the image
      //string username = "your-username"; // Replace with your username
      //string password = "your-password"; // Replace with your password
      Model.ApplicationSettings applicationSettings;

      Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss} 0.3.CameraHTTPClient starting");
#if RELEASE
         Console.WriteLine("RELEASE");
#else
      Console.WriteLine("DEBUG");
#endif
      // load the app settings into configuration
      var configuration = new ConfigurationBuilder()
           .AddJsonFile("appsettings.json", false, true)
      .AddUserSecrets<Program>()
      .Build();

      applicationSettings = configuration.GetSection("ApplicationSettings").Get<Model.ApplicationSettings>();

      using (HttpClientHandler handler = new HttpClientHandler { Credentials = new NetworkCredential(applicationSettings.Username, applicationSettings.Password) })
      using (HttpClient client = new HttpClient(handler))
      {
         try
         {
            HttpResponseMessage response = await client.GetAsync(applicationSettings.CameraUrl);
            response.EnsureSuccessStatusCode();

            byte[] imageBytes = await response.Content.ReadAsByteArrayAsync();
            await File.WriteAllBytesAsync(applicationSettings.SavePath, imageBytes);

            Console.WriteLine("Image retrieved and saved successfully.");
         }
         catch (Exception ex)
         {
            Console.WriteLine($"An error occurred: {ex.Message}");
         }
      }
   }
}

namespace Model
{
   public class ApplicationSettings
   {
      public string CameraUrl { get; set; } = "";

      public string SavePath { get; set; } = "Images";

      public string Username { get; set; } = "";

      public string Password { get; set; } = "";
   }
}

My Visual Studio 2022 solution with a project for each Copilot generated sample.

Summary

The Copilot generated code for my three “trivial” PoC applications compiled and worked with minimal modifications.

The Copilot generated code in this post is not suitable for production