Author Archives: purpleblob

Installing nanoFramework on the ATOM Lite ESP32 (M5Stack)

I have a wonderful little M5Stack ATOM Lite, ESP32 based dev kit to play with and as I’d had such success with the M5Core2 and nanoFramework, I thought I’d try the framework on the ATOM lite.

You, can check the device and the “Firmware Target” for the device from Recommended devices to start with .NET nanoFramework. So, for this device the target is ESP32_PICO.

If we connect your device to your computer’s USB port (hopefully the device will be recognised, if not see my previous post on setting up the M5Core2) execute the following command from the CLI, we’ll flash the device with the nanoFramework

nanoff --target ESP32_PICO --update --serialport COM10

Change the COM port to whatever your device is on. Also I’m again assuming you’ve installed nanoff, if not try running the following from the CLI “dotnet tool install -g nanoff”.

The ATOM lite comes with WiFi, bluetooth a NeoPixel RGB LED, button and even infrared.

Once you’ve installed nanoFramework, create a new nanoFramework project in Visual Studio 2022 (seem my previous posts on setting this up if you’ve not already got everything setup).

Let’s start with the LED, we’ll simply change the colour of the LED. First, you’ll need to add the package nanoFramework.AtomLite via NuGet. Next copy and paste this code into the Program.cs

while (true)
{
    AtomLite.NeoPixel.Image.SetPixel(0, 0, Color.Gray);
    AtomLite.NeoPixel.Update();

    Thread.Sleep(5000);

    AtomLite.NeoPixel.Image.SetPixel(0, 0, Color.Green);
    AtomLite.NeoPixel.Update();

    Thread.Sleep(5000);

    AtomLite.NeoPixel.Image.SetPixel(0, 0, Color.Red);
    AtomLite.NeoPixel.Update();
}

AdaptiveTrigger working in MAUI 7.x

This is just a quick update to my post Responsive, Reactive, Adaptive design in MAUI. The AdaptiveTrigger now works, so we can create adaptive UI’s like this

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="AdaptiveTriggerTest.MainPage">
    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup x:Name="Responsive">
            <VisualState x:Name="Large">
                <VisualState.StateTriggers>
                    <AdaptiveTrigger MinWindowWidth="1200" />
                </VisualState.StateTriggers>
                <VisualState.Setters>
                    <Setter Property="BackgroundColor" Value="Blue"/>

                    <Setter TargetName="MainLabel" Property="Label.TextColor" Value="Green"/>
                    <Setter TargetName="MainLabel" Property="Label.FontSize" Value="128"/>
                </VisualState.Setters>
            </VisualState>
            <VisualState x:Name="Default">
                <VisualState.StateTriggers>
                    <AdaptiveTrigger MinWindowWidth="0" />
                </VisualState.StateTriggers>
                <VisualState.Setters>
                    <Setter Property="BackgroundColor" Value="Azure"/>

                    <Setter TargetName="MainLabel" Property="Label.TextColor" Value="Red"/>
                    <Setter TargetName="MainLabel" Property="Label.FontSize" Value="48"/>
                </VisualState.Setters>
            </VisualState>
        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>

    <VerticalStackLayout
            Spacing="25"
            Padding="30,0"
            VerticalOptions="Center">

        <Label
                x:Name="MainLabel"
                Text="Hello, World!"
                SemanticProperties.HeadingLevel="Level1"
                FontSize="32"
                HorizontalOptions="Center" />
    </VerticalStackLayout>
</ContentPage>

The AdaptiveTrigger MinWindowWidth=”1200″ basically designates what the UI style etc. is for Window’s with a width >= 1200. The second AdaptiveTrigger is for anything smaller.

Turning your M5Core2 into a nanoFramework based web server

Like most of my posts regarding nanoFramework and the M5Core2, I owe a debt to those who created this stuff and I’m really just going through some of the samples etc. Trying them out and documenting my findings. This post is no different, it’s based on the Welcome to the .NET nanoFramework WebServer repository

Add the NuGet package nanoFramework.WebServer to your nanoFramework project.

You’ll need to also include code to connect to your WiFi, so checkout my post on that subject – Wifi using nanoFramework on the M5Core2.

Assuming you’ve connected to your WiFi, we can set up a WebServer like this

using var server = new WebServer(80, HttpProtocol.Http, new[] { typeof(PowerController) });
server.Start();

Thread.Sleep(Timeout.Infinite);

The first line supplies an array of controllers, so you can have multiple controllers for your different endpoints. In this case we’ve just got the single controller PowerController. This is a simple class that includes RouteAtrribute and MethodAttribute adorned methods to acts as routes/endpoints.

Let’s look at the PowerController, which just returns some M5Core2.Power values when accessed via http://m5core2_ip_address/power.

public class PowerController
{
   [Route("power")]
   [Method("GET")]
   public void PowerRoute(WebServerEventArgs e)
   {
      var power = M5Core2.Power;
           
      var sb = new StringBuilder();
      sb.AppendLine("Power:");
      sb.AppendLine($"  Adc Frequency: {power.AdcFrequency}");
      sb.AppendLine($"  Adc Pin Current: {power.AdcPinCurrent}");
      sb.AppendLine($"  Adc Pin Current Setting: {power.AdcPinCurrentSetting}");
      sb.AppendLine($"  Adc Pin Enabled: {power.AdcPinEnabled}");
      sb.AppendLine($"  Batt. Temp. Monitor: {power.BatteryTemperatureMonitoring}");
      sb.AppendLine($"  Charging Current: {power.ChargingCurrent}");
      sb.AppendLine($"  Charging Stop Threshold: {power.ChargingStopThreshold}");
      sb.AppendLine($"  Charging Voltage: {power.ChargingVoltage}");
      sb.AppendLine($"  Dc Dc1 Voltage: {power.DcDc1Voltage.Millivolts} mV");
      sb.AppendLine($"  Dc Dc2 Voltage: {power.DcDc2Voltage.Millivolts} mV");
      sb.AppendLine($"  Dc Dc3 Voltage: {power.DcDc3Voltage.Millivolts} mV");
      sb.AppendLine($"  EXTEN Enable: {power.EXTENEnable}");
      sb.AppendLine($"  VOff Voltage: {power.VoffVoltage}");
      sb.AppendLine($"  Gpio0 Behavior: {power.Gpio0Behavior}");
      sb.AppendLine($"  Gpio0 Value: {power.Gpio0Value}");

      e.Context.Response.ContentType = "text/plain";
      WebServer.OutPutStream(e.Context.Response, sb.ToString());
}

As you can see from the last line of code, we send the response back with our payload, the string of power information.

We can also return HTTP codes using

WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.OK);

This is great, but what’s the IP address of our IoT device, so I can access the web server?

Well, ofcourse you could check your router or DHCP server, but better still, let’s output the IP address to the M5Core2 screen using

Console.WriteLine(IPGlobalProperties.GetIPAddress().ToString());

We can support multiple routes per method, such as

[ublic class PowerController
{
[Route("power")]
[Route("iotpower")]
[Method("GET")]
public void PowerRoute(WebServerEventArgs e)
{
// code removed
}

Note: Routes are usually case insensitive, unless you add the CaseSensitiveAttribute to your method.

Interacting with the M5Core2 Accelerometer and Gryoscope using nanoFramework

The M5Core includes an accelerometer which allows us to measure the rate of acceleration, as well as a gyroscope to sense angular movement.

We initialize the combined AccelerometerGyroscope and calibrate it by using the following code. The number, 100 in this case, is the number of iterations to calibrate the AccelerometerGyroscope

M5Core2.AccelerometerGyroscope.Calibrate(100);

Let’s look at the code to read the accelerometer and gyroscope (we’ll also read the internal temperature of the AccelerometerGyroscope)

Console.Clear();

M5Core2.AccelerometerGyroscope.Calibrate(100);

while (true)
{
   var accelerometer = M5Core2.AccelerometerGyroscope.GetAccelerometer();
   var gyroscope = M5Core2.AccelerometerGyroscope.GetGyroscope();
   var temperature = M5Core2.AccelerometerGyroscope.GetInternalTemperature();

   Console.CursorLeft = 0;
   Console.CursorTop = 1;

   Console.WriteLine("Accelerator:");
   Console.WriteLine($"  x={accelerometer.X}");
   Console.WriteLine($"  y={accelerometer.Y}");
   Console.WriteLine($"  z={accelerometer.Z}");
   Console.WriteLine("Gyroscope:");
   Console.WriteLine($"  x={gyroscope.X}");
   Console.WriteLine($"  y={gyroscope.Y}");
   Console.WriteLine($"  z={gyroscope.Z}");
   Console.WriteLine("Internal Temp:");
   Console.WriteLine($"  Celsius={temperature.DegreesCelsius}");

   Thread.Sleep(20);
}

nanoFramework Console (using the M5Core2)

The nanoFramework comes with a Console class, for the M5Stack this is in the namespace nanoFramework.M5Stack.Console

Before we uses the Console we need to initialize the screen, this essentially creates the screen buffer and assigns a font from the application’s resource. As I’m testing this stuff on the M5Core2, the code looks like this.

M5Core2.InitializeScreen();

Now we can simply use the Console like we would for a Console application on Windows.

// clear the console
Console.Clear();

// output some test
Console.WriteLine("Some Text");

// change the foreground colour
Console.ForegroundColor = Color.Red;
Console.WriteLine("Some Red Text");

// change foreground and background colours
Console.BackgroundColor = Color.Yellow;
Console.ForegroundColor = Color.White;
Console.WriteLine("Some Green Text on Yellow Background");

We can also change the font by supplying a font resource, the default included is consolas_regular_16.tinyfnt. We would add the font as a resource and create the font like this

Console.Font = Resource.GetFont(Resource.FontResources.consolas_regular_16);

We can move the cursor around using

Console.CursorLeft = 3;
Console.CursorTop = 5;

We can also get the height and width of our window via the Console using

Console.WriteLine($"Height: {Console.WindowHeight}, Width: {Console.WindowWidth}");

nanoFramework accessing a webservice using the M5Core2

In the previous post Wifi using nanoFramework on the M5Core2 we looked at connecting our M5Core2 using it’s wireless network capability, to our WiFi network. Next, let’s look at how we access a website, for example I’ll try to access the Cheer Lights API.

We’ll need to add the NuGet package nanoFramework.System.Net.Http.Client which exposes the HttpClient functionality for the nanoFramework.

Just like the full .NET framework/and core. We should create an HttpClient for the lifetime of the application. So we’d have something like this

public static class CheerLights
{
   private static readonly HttpClient HttpClient = new HttpClient();
}

The Cheer Lights API is a simple call to https://api.thingspeak.com/channels/1417/field/2/last.txt which will return a #hex colour, for example #008000. So. we might write some code, such as this in a funtion within the CheerLights class

var requestUri = "https://api.thingspeak.com/channels/1417/field/2/last.txt";

var response = HttpClient.Get(requestUri);
response.EnsureSuccessStatusCode();
var responseBody = response.Content.ReadAsString();

This would work for a non-HTTPS site, but for HTTPS requires that we get the CA certificate for the site we want to interact with (I haven’t yet found a way to use HTTPS without this).

To get the certificate, navigate to the page using a browser (I’m using Microsoft EDGE). Click on the padlock, click on connection is secure, then click on the show certificate button. Select Details then in the Certificate Hierarchy select the root CA certificate and export this (renaming as a txt file). This will be what we used for the HttpsAuthentCert (as we’ll see in a moment).

Now we want to include the certificate which we can do as a resource or just embedding the text into the code. So now we’d have something like this

try
{
   HttpClient.HttpsAuthentCert = new X509Certificate(
                        @"-----BEGIN CERTIFICATE-----
MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD
QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT
MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB
CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97
nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt
43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P
T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4
gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO
BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR
TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw
DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr
hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg
06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF
PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls
YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk
CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=
-----END CERTIFICATE-----");

   var requestUri = "https://api.thingspeak.com/channels/1417/field/2/last.txt";
   var response = HttpClient.Get(requestUri);
   response.EnsureSuccessStatusCode();
   var responseBody = response.Content.ReadAsString();

Wifi using nanoFramework on the M5Core2

When we’re creating an IoT device, the likelihood is we’ll want to connect to Wifi and the internet (well it’s sort of in the IoT name isn’t it).

Please note, this post is heavily based upon the nanoFramework samples, so do go and view them for a whole host of excellent sample code.

The nanoFramework comes with a NuGet package named nanoFramework.System.Device.Wifi which you’ll need to add to your project.

Now we can use the WifiNetworkHelper class to connect to your DHCP server (your router generally) using the ConnectDhcp method. Now this will also save the configuration of the network ssid and password.

const string ssid = "YOUR_SSID";
const string password = "YOUR_WIFI_PASSWORD";

var cs = new CancellationTokenSource(60000);
var success = WifiNetworkHelper.ConnectDhcp(ssid, password, requiresDateTime: true, token: cs.Token);
if (!success)
{
    Debug.WriteLine($"Cannot connect to the WiFi, error: {WifiNetworkHelper.Status}");
    if (WifiNetworkHelper.HelperException != null)
    {
        Debug.WriteLine($"ex: {WifiNetworkHelper.HelperException}");
    }
}
else
{
   Debug.WriteLine("Connected successfully");
}

As mentioned, the ConnectDhcp method saves our configuration, so once that’s happened, we can switch to using the Reconnect method instead, i.e.

var cs = new CancellationTokenSource(60000);
var success = WifiNetworkHelper.Reconnect(requiresDateTime: true, token: cs.Token);
if (!success)
{
    Debug.WriteLine($"Cannot connect to the WiFi, error: {WifiNetworkHelper.Status}");
    if (WifiNetworkHelper.HelperException != null)
    {
        Debug.WriteLine($"ex: {WifiNetworkHelper.HelperException}");
    }
}
else
{
   Debug.WriteLine("Connected successfully");
}

The ssid and password are stored using the Wireless80211Configuration on the device. So we can check if our configuration has been stored – this ofcourse also means we can get access to the configuration – beware if security is an issue that these are then available as plain text

var configuration = Wireless80211Configuration.GetAllWireless80211Configurations();
foreach (var config in configuration)
{
   Debug.WriteLine($"SSID: {config.Ssid}, Password: {config.Password}");
}

.NET nanoframework on an M5Core2

Every now and then I get the desire to do some electronics – I’m not very good at it, so it often ends up with me hitting a wall where I need to learn more electronics. Anyway, the devices available now are truly amazing. Before I used Raspberry Pi’s which are amazing, but now I’m using the Raspberry Pi Pico W and just got an even more (although ofcourse more expensive) amazing piece of kit in the M5Core2.

This thing has a touch screen, USB-C, the obligatory LED, vibration, microphone, speaker and more. But what’s really interesting is that it’s supported by .NET nanoframework.

Note: The pico also has a port for using with nanoframework, but for now I’m leaving that set-up with MicroPython.

Getting Started

Let’s start with the hardware

  • Plug your device into a USB port on your computer
  • If the device is not detected, got to Driver Installation and select the latest driver for your OS
  • If all went well it will appear on a COM port. In my case I’m on Windows I checked whether the COM PORT was added to your device manager, mine’s on COM9

The power button is on the left-hand side of the M5Core2 at the top, press this once and you should see a wonderful UI full of bits and bobs which detects sound and movement, sadly we’re going to get rid of that when we install nanoframework – so have a play now whilst you can.

Note: To turn the M5Core2 off, hold the power button for 6 seconds.

Getting nanoframework up and running

Follow the steps on the nanoframework Getting Started Guild – Managed (C#).

You’ll want to install the nano firmware flasher/nanoff using

dotnet tool install -g nanoff

You’re going to want to flash the M5Core2 using nanoff like this (remember this will clear the M5Core2 so if you want to play with the installed app first do so now – as you can tell I wish I’d played with the the default app a little more)

nanoff --target M5Core2 --update --serialport COM9

Replace the COM9 port name with whatever port your device is on.

Writing the LED Blink App.

I’m going be using Visual Studio 2022 to write my code. So, go to the extensions menu item and install the nanoframework extension. This will install project templates and other goodies.

Note: I found this didn’t work great first time, so had to try to remove and do again, but got there in the end, so persevere if it fails first time.

Although the M5Core2 has a screen, the ubiquitous first application for a microcontroller is the LED blinking application. Now the M5Core2 does have an LED but it’s the power LED, but that’s not an issue nanoframework has our back.

Once the extension is installed and working, creating a new application using the Blank Application (.NET nanoframework) template, you’ll see some code which looks like any other C# project. Actually, nanoframework it supports top level statements, so we’ll look at changing this code in a moment.

First off, we need to install this NuGet package

nanoframework.M5Core2

It will install quite a few extra packages.

Replace everything with Program.cs with the following

using nanoFramework.M5Stack;
using Console = nanoFramework.M5Stack.Console;

M5Core2.InitializeScreen();

Console.WriteLine("Led ON");
M5Core2.PowerLed = false;

In the above code I’m showing how we can write the M5Core2 LCD screen using Console (we need to initialize the screen to begin with) and I’m setting the PowerLed to false (umm yes, false seems to be ON, not sure if this is a bug or if this is some electronics things).

Before we can run anything, locate the Device Explorer view. In Visual Studio either type this into the Search (Ctrl+Q) textbox or go to View | Other Windows | Device Explorer. Ensure your hardware is plugged into a USB port and powered up and if need be, refresh the list in the Device Explorer until you see something like M5Core2 @ COM9. Feel free to ping the device using the ping button to check it’s running nanoCLR (this will be displayed in the Output Window of Visual Studio, .NET nanoFramework extension option).

Now press the run button (assuming .NET nanoFramework Device is selected). In the Output Window, Debug view, you’ll see the compiler run then the deployment process takes place, or in my case, it doesn’t work straight away. If yours works, jump ahead and ignore the bit on fixing the error I had.

If you get Error: a3000000, I found this was version issue, noticing that it also says Link failure: some assembly references cannot be resolved!!. If you check the Error List Window you may see it mentions a version issue, and suggests using AutoGenerateBindingRedirects. It appears nanoFramework does not support assembly binding, see this link.

So we need to check the Output Debug window, I found that after the Error: a3000000 I had this

Assembly: Iot.Device.Axp192 (1.2.0.0) needs assembly 'UnitsNet.Power' (4.145.0.0)

But before he Error: a3000000 I also have all these

Assembly: Blinky (1.0.0.0) needs assembly 'nanoFramework.M5Core2' (1.1.82.38397)
Assembly: nanoFramework.M5Core2 (1.1.82.38397) needs assembly 'Iot.Device.Mpu6886' (1.2.0.0)
Assembly: nanoFramework.M5Core2 (1.1.82.38397) needs assembly 'Iot.Device.Rtc' (1.2.0.0)
Assembly: nanoFramework.M5Core2 (1.1.82.38397) needs assembly 'Iot.Device.Axp192' (1.2.0.0)
Assembly: Iot.Device.Mpu6886 (1.2.0.0) needs assembly 'UnitsNet.Temperature' (4.145.0.0)
Assembly: Iot.Device.Rtc (1.2.0.0) needs assembly 'UnitsNet.Temperature' (4.145.0.0)
Assembly: Iot.Device.Axp192 (1.2.0.0) needs assembly 'UnitsNet.ElectricPotential' (4.145.0.0)
Assembly: Iot.Device.Axp192 (1.2.0.0) needs assembly 'UnitsNet.ElectricCurrent' (4.145.0.0)
Assembly: Iot.Device.Axp192 (1.2.0.0) needs assembly 'UnitsNet.Temperature' (4.145.0.0)

Obviously, there’s a versioning problem so we need to go into the NuGet Package manager and go through each of the items listed and change to the expected versions, either rolling back or forward out packages. I basically updated everything on the left hand side to of the list (above) to the latest versions and all, eventually started to work.

Once everything builds and deploys via the run button your M5Core2 should show the text “Led OFF” and the Power Led is off. Now switch the code to the following

Console.WriteLine("Led ON");
M5Core2.PowerLed = false;

and run again.

Once deployed and started on the device, the Power Led should be on and the text on the screen will say “Led ON”.

We’ve now got the bare bones of a .NET nanoFramework application running on the ESP32 M5Core2.

References

Nanoframework M5Stack

MicroPython on the Raspberry Pi Pico

So I bought myself a “Wireless Plasma Kit” which included the Raspberry Pi Pico W as the controller. Now a long time back (probably when they were first introduced) I played with the non-wireless Pico, but have pretty much forgotten everything around it. First off I accidentally removed all the software off of my new Pico W and then had to remind myself/learn to put things back together, so let’s look at this. It’s pretty simple but like most things, it is when you know how.

Note: The first item in the list below will wipe your Pico (and yes, this is exactly what I did not realising that software was already installed – correct, I didn’t read the guide on setting things up.

  • Push and hold the BOOTSEL button and plug the pico’s USB connect into your computer, then release the BOOTSEL. If all went well your pico will appear like an attached USB driver
  • Locate and download the MicroPython UF2 file, for example the latest UF2 for the Pico W is available here http://www.micropython.org/download/rp2-pico-w/ – if you’re not using the Pico W look for the UF2 for your device, i.e. the non-W Pico.
  • Drag the UF2 onto the Pi USB folder. At this point the pico will reboot

The UF2 file is USB Flashing Format file and in this case is a bootloader. MicroPython sets up a MicroPython REPL, so you can type commands directly to it via the assigned COM port.

Okay, so MicroPython is installed, what next?

Depending on your preference, there are editors such as Thonny and also an extension to VS Code (and others). I prefer VS Code, but Thonny seemed simpler to interact with the pico, so I went with that in the end.

In Thonny, if connected okay you should see a Shell with the MicroPython REPL running, just type in the following, followed by the enter key

print("Hello World")

If all went well the REPL should write back Hello World

If you weren’t able to see the REPL or got no response try choosing or configuring the interpreter (bottom right of Thonny, click on the option there). Mine says MicroPython (Raspberry Pi Pico) COM8.

Start up

When the Pico starts, i.e. first plugged in, it looks for the file /boot.py. If one exists it’ll run this to set any low-level code. Whilst you can supply one of these files, more likely you’ll add a /main.py which is run after /boot.py and this should contain your main script that should run when the board is restarted or reset.

Note: Remember, the main.py will run and end unless you explicitly loop within it. So, if you output “Hello World” (for example) then that’s all the Pico will do and then essentially sits there doing nothing.

Let’s add the hardware equivalent of Hello World, i.e. turn on the Pico onboard LED, type/paste the following into the REPL

from machine import Pin
#led = Pin(25, Pin.OUT) # On the Pico not W
led = Pin("LED", Pin.OUT)
led.value(1)

Or try this, to flash the LED

import time
from machine import Pin

#led = Pin(25, Pin.OUT) # On the Pico not W
led = Pin("LED", Pin.OUT)

while True:
    led.toggle()    
    time.sleep(1.0)  

So when you use Ctrl+D from Thonny to soft reset, now the Pico should restart and blink the LED.

References

Raspberry Pi Pico Python SDK
Connecting to the Internet with Pico W
MicroPython, Quick Reference

Responsive, Reactive, Adaptive design in MAUI

Why do we need to be responsive, reactive/adaptive?

Initially, when Apple introduced the first iPhone (and to be honest way before that with things like Windows CE) we had a fairly small and fixed sized device. Implementing applications for this, from a UI perspective, was fairly simple in that the constraints of the device size meant our applications wouldn’t need to dynamically change to different device sizes etc.

As time went by, we got tablets. Some applications weren’t rewritten for tablet and the UI would simply get scaled up and hence the UI looked like the phone UI just on a larger screen. In truth we can see the same issue with portrait and landscape modes. Some applications simply turn off the option to support landscape to force the user to stick with the UI design for portrait, some applications simply ignore the fact the screen has been rotated and use the same layout for portrait and landscape.

Now with OS’s supporting side by side docking, we can no longer just think of our device screen size as being static. Instead docking an application on something like my Samsung S6, where the application was designed for a tablet now needs to also take into account the potential of the application being docked.

Okay, that’s all a long-winded way of saying. To make truly cross platform UI and usable applications we need to think about our, previously static dimensioned, applications and resizable. So, basically just like on a desktop application.

What do we need our application to do?

Let’s look at some goals for our MAUI (or any framework) application to start to fulfill our needs. These are discussed in my previous post Introduction to Responsive, Reactive, Adaptive design but let’s now look into these concepts with a view to how we might implement such things.

  • Element styles – this may relate to any style used, but to be specific, let’s look at fonts as an example. We need to change our font sizes for the different sized devices. I mean a 24 pt font might look great on mobile but becomes a small island of text in a larger landscape of a tablet or desktop app.
  • Element Sizes – we need a way to change the sizes of elements to suit the size of the device. For example, a button with a fixed size for mobile will become lost on larger screens. Ofcourse MAUI, WPF etc. come with grid and other layouts that can help here.
  • Element Positioning – we need a way to move elements around the screen. Displaying buttons on the bottom of the screen in portrait mode may look great, but when switch to landscape, maybe they need to be repositioned to the side of the screen (for example).
  • Changing layouts – taking a mobile app that has two section, one page with a list with navigation items and another page that displays when click by the navigation items is great on mobile but for tablet or desktop, would be better if the navigation page become a docked panel on the left of the screen and clicking links shows the navigated page on the right of the screen – this is a pretty standard layout change you’ll see in lots of apps.

Note: This post is primarily aimed at MAUI, however the concepts etc. are valid for other UI frameworks and also are not limited to mobile devices. Desktop applications can also offer better user experiences if they can adapt to the size of the window displaying them.

Before looking into some code example etc. Create yourself a MAUI application (if you want to follow along) and we’re going to solely. I’m going to run on Windows and run the MAUI application as a Windows application, because I can easily resize it to see the effects of my code. Ofcourse if we get things working on a Windows desktop application, we should have no trouble with the same code on mobile devices (as long as we choose the right style options etc.).

Element Styles

This should be the simplest thing to implement. We’ve got a MainPage (in MAUI) with some text on it, so the code looks like this

<Grid>
   <Label 
      Text="My Application"
      VerticalOptions="Center" 
      HorizontalOptions="Center" />
</Grid>

Now we’ll stick to running this as a Windows application as this will allow us to dynamically resize our window/view. If you run your MAUI app the text will be an okay size in the middle of the screen, but we want to be able to change the label’s font size based upon the size of the window/device.

We know that we can use OnIdiom along with a resource to set the FontSize like this

Note: I’ve extracted only the code which changes, so obviously you’ll need to copy this code to the correct places if you’re following along.

<ContentPage.Resources>
    <OnIdiom x:Key="FontSize" x:TypeArguments="x:Double" 
      Default="24" Phone="48" Tablet="96" Desktop="128"/>
</ContentPage.Resources>

<Label 
   Text="My Application"
   VerticalOptions="Center" 
   HorizontalOptions="Center" 
   FontSize="{StaticResource FontSize}"/>

In the above, if you run the app. in Windows you’ll get the Desktop assigned FontSize (128) and on a phone (or phone emulator) the text will displayed using that assigned FontSize. However, as you’ll have noticed, this only partially fulfills our requirements. It does display with different font sizes but it’s static. If you dock an app side by side on a tablet with this, the FontSize remains the same – as you probably expect as this is solely checking what the device/idiom is.

However, we now see that we can use resources to change things based upon some device value. We just need a way to respond to our Window size dynamically.

Responding the page size changes

MAUI 6.x does not have anything that handles this sort of thing for us, there is the AdaptiveTrigger, but in the Maui 6.x releases this does not work, so we will look at it later and it may eventually be the best solution, but for now I’m on 6.x and hence it’s unusable, so let’s proceed with what we currently have available.

The simplest way to achieve this, for now, is to write some code in our page’s code behind. If we change our MainPage code to look like this

public MainPage()
{
   InitializeComponent();
   SizeChanged += OnSizeChanged;
}

private void OnSizeChanged(object sender, EventArgs e)
{
   // code for resizing goes here
}

Let’s remove the OnIdiom code and the FontSize from the label in our previous example and now add a name to the label so it looks like this

<Label 
   x:Name="MainLabel"
   Text="My Application"
   VerticalOptions="Center" 
   HorizontalOptions="Center" />

Now change our new OnSizeChange method to have the following

MainLabel.FontSize = Width > 400 ? 128 : 48;

This is basically what we want to achieve, but this isn’t very reusable by other components or properties, but it works. Let’s move a step further towards our goals and change things so we can have our XAML change values for us. To do this, we’re going to switch to using the Visual State Manager (VSM). Change the OnSizeChange code to the following

VisualStateManager.GoToState(MainLabel, Width > 400 ? "Large" : "Default");

At this point we’re effectively moving the logic for setting the sizes etc. into our XAML. This is a good step forward, but we are still (at this time) tied to the MainLabel element (and this is not good). Bare with me.

Let’s look at how we would use this change in our XAML – change the Label to look like this

<Label x:Name="MainLabel"
   Text="My Application"
   VerticalOptions="Center" 
   HorizontalOptions="Center">
   <VisualStateManager.VisualStateGroups>
      <VisualStateGroup x:Name="Responsive">
         <VisualState x:Name="Large">
            <VisualState.Setters>
               <Setter Property="FontSize" Value="128" />
               <Setter Property="TextColor" Value="Green" />
            </VisualState.Setters>
         </VisualState>
         <VisualState x:Name="Default">
            <VisualState.Setters>
               <Setter Property="FontSize" Value="48" />
               <Setter Property="TextColor" Value="Red" />
            </VisualState.Setters>
         </VisualState>
     </VisualStateGroup>
  </VisualStateManager.VisualStateGroups>
</Label>

As previously stated, we are still bound to the x:Name visual element (which is not perfect) but we can now change any property in this named element, based upon the visual state as demonstrated by change FontSize and TextColor.

This approach also suffers a problem with multiple controls reusing the VisualState names such as x:Name=”Large”. There may be a way around this that I’ve not discovered yet.

Our end goal is for multiple controls changing based upon the VSM states. Let’s start by moving the VSM XAML to the ContentPage itself. This means we no longer care about the explicit element accepting the state, but it’s moved to the ContentPage and from there we reference the specific elements using TargetName

<VisualStateManager.VisualStateGroups>
   <VisualStateGroup x:Name="Responsive">
      <VisualState x:Name="Large">
         <VisualState.Setters>
            <Setter Property="BackgroundColor" Value="Blue"/>

            <Setter TargetName="MainLabel" Property="Label.TextColor" Value="Green"/>
            <Setter TargetName="MainLabel" Property="Label.FontSize" Value="128"/>
         </VisualState.Setters>
      </VisualState>
      <VisualState x:Name="Default">
         <VisualState.Setters>
            <Setter Property="BackgroundColor" Value="Azure"/>

            <Setter TargetName="MainLabel" Property="Label.TextColor" Value="Red"/>
            <Setter TargetName="MainLabel" Property="Label.FontSize" Value="48"/>

         </VisualState.Setters>
      </VisualState>
   </VisualStateGroup>
</VisualStateManager.VisualStateGroups>

<Grid>
   <Label x:Name="MainLabel"
      Text="My Application"
      VerticalOptions="Center" 
      HorizontalOptions="Center" />
</Grid>

We’ll again need to change the code behind, but now just need to send state changes to the page itself, i.e.

VisualStateManager.GoToState(this, Width > 400 ? "Large" : "Default");

As mentioned, what we’ve done is gained the ability to change, not only the label FontSize and TextColor but also the BackgroundColor of the Page and potentially anything else displayed within the page. We’ve also removed any knowledge of the controls displayed on the page from the code behind (i.e. removed the name MainLabel from the code behind).

One more issue we might want to look into is that we are currently coding the different breakpoint size i.e. Width > 400 into the code behind. It would be better if we could move this to the XAML or some other mechanism not requiring us to edit the code behind at all.

AdaptiveTrigger

One way of removing the VisualStateManager code and the handling of the SizeChanged event is to use the AdaptiveTrigger.

At the time of writing (6.x MAUI) this doesn’t work correctly, it appears to have been fixed in 7.x, so for now we cannot use the AdaptiveTrigger, but let’s take a look at the code changes that should work when it’s fixed.

We’d remove all the code we added to the page’s code behind and our XAML would look something like this

<VisualStateManager.VisualStateGroups>
   <VisualStateGroup x:Name="Responsive">
      <VisualState x:Name="Large">
         <VisualState.StateTriggers>
            <AdaptiveTrigger MinWindowWidth="1200" />
         </VisualState.StateTriggers>
         <VisualState.Setters>
            <Setter Property="BackgroundColor" Value="Blue"/>

            <Setter TargetName="MainLabel" Property="Label.TextColor" Value="Green"/>
            <Setter TargetName="MainLabel" Property="Label.FontSize" Value="128"/>
         </VisualState.Setters>
         </VisualState>
      <VisualState x:Name="Default">
         <VisualState.StateTriggers>
            <AdaptiveTrigger MinWindowWidth="0" />
         </VisualState.StateTriggers>
         <VisualState.Setters>
            <Setter Property="BackgroundColor" Value="Azure"/>

            <Setter TargetName="MainLabel" Property="Label.TextColor" Value="Red"/>
            <Setter TargetName="MainLabel" Property="Label.FontSize" Value="48"/>
         </VisualState.Setters>
      </VisualState>
   </VisualStateGroup>
</VisualStateManager.VisualStateGroups>

Lines such as <AdaptiveTrigger MinWindowWidth=”1200″ /> are effectively defining our UI breakpoints. We’ll discuss more around breakpoints in the next section.

Breakpoints

We’ve seen that whatever solution we try to use, we’ll need some way to define our breakpoints. The dimensions at which we change layouts etc. If we look to create breakpoints in line with bootstrap documentation, for example. Then we might declare some resources like this

<x:Double x:Key="ExtraSmall">0</x:Double>
<x:Double x:Key="Small">576</x:Double>
<x:Double x:Key="Medium">768</x:Double>
<x:Double x:Key="Large">992</x:Double>
<x:Double x:Key="ExtraLarge">1200</x:Double>
<x:Double x:Key="ExtraExtraLarge">1400</x:Double>

and use these in our AdaptiveTrigger’s.

Great, but what can we use now?

As stated several times, as of MAUI 6.x we cannot use AdaptiveTrigger’s, so what can we do which won’t end up requiring us to write code-behind etc.? Well, if we stick to using the VSM, then we need a way to attach to the ContentPage and a way to use the VSM to trigger our UI changes.

One way to achieve this is by creating a behavior for the ContentPage type something like this

public class BreakpointBehavior : Behavior<Page>
{
    protected override void OnAttachedTo(Page page)
    {
        page.SizeChanged += PageSizeChanged;
        base.OnAttachedTo(page);
    }

    protected override void OnDetachingFrom(Page page)
    {
        page.SizeChanged += PageSizeChanged;
        base.OnDetachingFrom(page);
    }

    private void PageSizeChanged(object sender, EventArgs e)
    {
        if (sender is Page page)
        {
            VisualStateManager.GoToState(page, ToState(page.Width));
        }
    }

    private string ToState(double width)
    {
        if (width >= 1400)
            return "ExtraExtraLarge";
        if (width >= 1200)
            return "ExtraLarge";
        if (width >= 992)
            return "Large";
        if (width >= 768)
            return "Medium";
        if (width >= 576)
            return "Small";

        return "ExtraSmall";
    }
}

and now our ContentPage would look like this

<ContentPage.Behaviors>
   <extensions:BreakpointBehavior />
</ContentPage.Behaviors>

<VisualStateManager.VisualStateGroups>
   <VisualStateGroup x:Name="Responsive">
      <VisualState x:Name="ExtraExtraLarge">
         <VisualState.Setters>
            <Setter Property="BackgroundColor" Value="Blue"/>

            <Setter TargetName="MainLabel" Property="Label.TextColor" Value="Pink"/>
            <Setter TargetName="MainLabel" Property="Label.FontSize" Value="128"/>
         </VisualState.Setters>
      </VisualState>
      <VisualState x:Name="Large">
         <VisualState.Setters>
            <Setter Property="BackgroundColor" Value="Blue"/>

            <Setter TargetName="MainLabel" Property="Label.TextColor" Value="Green"/>
            <Setter TargetName="MainLabel" Property="Label.FontSize" Value="96"/>
         </VisualState.Setters>
      </VisualState>
      <VisualState x:Name="Medium">
         <VisualState.Setters>
            <Setter Property="BackgroundColor" Value="Azure"/>

            <Setter TargetName="MainLabel" Property="Label.TextColor" Value="Red"/>
            <Setter TargetName="MainLabel" Property="Label.FontSize" Value="48"/>

         </VisualState.Setters>
      </VisualState>
   </VisualStateGroup>
</VisualStateManager.VisualStateGroups>

<Grid>
   <Label x:Name="MainLabel"
      Text="My Application"
      VerticalOptions="Center" 
      HorizontalOptions="Center" />
</Grid>

Essentially, we add visual states to match what the BreakpointBehvior sets and change our UI accordingly. We might look to make additions to the behavior to allow us to set the breakpoint properties via our XAML that way, we can respond to custom defined breakpoints.

We’ve covered a lot of ground but only really looked at the possibilities for responsive design, i.e. we can change properties but we’re not changing the layout.
Also, this code does not handle portrait or landscape orientations.

Changing layouts altogether

Whilst we can change properties on our layouts using the example shown here. It really would be much simpler if we could simply change layouts altogether and design the different layouts required separately. This is definitely useful when looking at portrait/landscape changes.

For Xamarin Forms I had a simple way of handling this, it may not be the best way or efficient, but it allowed me to host different views in a ContentView in a very simple way.

See my post Handling orientation in Xamarin Forms for the code of OnOrientation. This code works with MAUI.

Our XAML might look something like this

<extensions:OnOrientation>
   <extensions:OnOrientation.Portrait>
      <VerticalStackLayout>
         <Label Text="My Application Portrait"
            VerticalOptions="Center" 
            HorizontalOptions="Center" />
      </VerticalStackLayout>
   </extensions:OnOrientation.Portrait>
   <extensions:OnOrientation.Landscape>
      <Grid>
         <Label Text="My Application Landscape"
            VerticalOptions="Center" 
            HorizontalOptions="Center" />
      </Grid>
   </extensions:OnOrientation.Landscape>
</extensions:OnOrientation>

So, what about an alternative to the code above?

Changing layouts using DataTemplates and DataTemplateSelector

MAUI, WPF etc. already has the ability to define alternate layouts templates. We can create DataTemplate resources, one for portrait, one for landacape and then use AdaptiveTriggers or VSM to be used based upon the device orientation. We can then define a DataTemplateSelector to simply switch in and out the template based upon orientation (a bit like my OnOrientation code, above).

Let’s forget about the device info telling us what the orientation of the device is, but instead we’ll simply define landscape ad having a Width great then the height otherwise it’s in portrait orientation.

We’re going to now start to put together some of the pieces we’ve already discussed…

Sorry, this is a bigger chunk of code, but our ContentPage should now look like the following

    <ContentPage.Resources>
        <DataTemplate x:Key="Portrait">
            <VerticalStackLayout>
                <VisualStateManager.VisualStateGroups>
                    <VisualStateGroup x:Name="Responsive">
                        <VisualState x:Name="Medium">
                            <VisualState.Setters>
                                <Setter Property="BackgroundColor" Value="Blue"/>

                                <Setter TargetName="MainLabel" Property="Label.TextColor" Value="Green"/>
                                <Setter TargetName="MainLabel" Property="Label.FontSize" Value="128"/>
                            </VisualState.Setters>
                        </VisualState>
                        <VisualState x:Name="Small">
                            <VisualState.Setters>
                                <Setter Property="BackgroundColor" Value="Azure"/>

                                <Setter TargetName="MainLabel" Property="Label.TextColor" Value="Red"/>
                                <Setter TargetName="MainLabel" Property="Label.FontSize" Value="48"/>

                            </VisualState.Setters>
                        </VisualState>
                    </VisualStateGroup>
                </VisualStateManager.VisualStateGroups>

                <Label x:Name="MainLabel"
                       Text="My Application Portrait"
                       VerticalOptions="Center" 
                       HorizontalOptions="Center" />
            </VerticalStackLayout>
        </DataTemplate>
        <DataTemplate x:Key="Landscape">
            <Grid>
                <VisualStateManager.VisualStateGroups>
                    <VisualStateGroup x:Name="Responsive">
                        <VisualState x:Name="ExtraExtraLarge">
                            <VisualState.Setters>
                                <Setter Property="BackgroundColor" Value="Blue"/>

                                <Setter TargetName="MainLabel" Property="Label.TextColor" Value="Green"/>
                                <Setter TargetName="MainLabel" Property="Label.FontSize" Value="128"/>
                            </VisualState.Setters>
                        </VisualState>
                        <VisualState x:Name="Medium">
                            <VisualState.Setters>
                                <Setter Property="BackgroundColor" Value="Azure"/>

                                <Setter TargetName="MainLabel" Property="Label.TextColor" Value="Red"/>
                                <Setter TargetName="MainLabel" Property="Label.FontSize" Value="48"/>

                            </VisualState.Setters>
                        </VisualState>
                    </VisualStateGroup>
                </VisualStateManager.VisualStateGroups>

                <Label x:Name="MainLabel"
                       Text="My Application Landscape"
                       VerticalOptions="Center" 
                       HorizontalOptions="Center" />
            </Grid>
        </DataTemplate>
        <extensions:OrientationDataTemplateSelector x:Key="OrientedView"
            Landscape="{StaticResource Landscape}" 
            Portrait="{StaticResource Portrait}" />
    </ContentPage.Resources>

    <ContentPage.Behaviors>
        <extensions:AdaptableBehavior OrientationTemplateSelector="{StaticResource OrientedView}" />
    </ContentPage.Behaviors>

There’s a lot there, but hopefully it makes sense. We’re defining two data templates, the first for Portrait, the second for Landscape, we’re also defining state changes based upon some breakpoints, Medium and Small. We also define the actual UI/layout within each template.

Next, we’re using a DataTemplateSelector, that I’ve created, named OrientationDataTemplateSelector. This will simply choose the data template based upon the orientation (width against height) of the window/view. The code for this looks like this

public class OrientationDataTemplateSelector : DataTemplateSelector
{
    public DataTemplate Landscape { get; set; }
    public DataTemplate Portrait { get; set; }

    protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
    {
        return item?.ToString() == "Portrait" ? Portrait : Landscape;
    } 
}

This is just an example, you might prefer to use enum’s for the value, for example.

The DataTemplateSelector is simply a way to say use this template for landscape and this one for portrait but we now need a way to tell the selector when to use each of these data templates. This is what I’ve created the AdaptableBehavior for. Here’s the code

public class AdaptableBehavior : Behavior<ContentPage>
{
    public static readonly BindableProperty OrientationTemplateSelectorProperty = BindableProperty.Create(nameof(OrientationTemplateSelector),
        typeof(DataTemplateSelector), typeof(OrientationLayout));

    public DataTemplateSelector OrientationTemplateSelector
    {
        get => (DataTemplateSelector)GetValue(OrientationTemplateSelectorProperty);
        set => SetValue(OrientationTemplateSelectorProperty, value);
    }

    private static View CreateItemView(object item, DataTemplate dataTemplate)
    {
        if (dataTemplate != null)
        {
            var view = (View)dataTemplate.CreateContent();
            view.BindingContext = item;
            return view;
        }

        return new Label { Text = item?.ToString(), HorizontalTextAlignment = TextAlignment.Center };
    }


    protected override void OnAttachedTo(ContentPage page)
    {
        page.SizeChanged += PageSizeChanged;
        base.OnAttachedTo(page);
    }

    protected override void OnDetachingFrom(ContentPage page)
    {
        page.SizeChanged += PageSizeChanged;
        base.OnDetachingFrom(page);
    }

    private void PageSizeChanged(object sender, EventArgs e)
    {
        if (sender is ContentPage page)
        {
            var orientation = GetOrientation(page);
            var dataTemplate = OrientationTemplateSelector;
            var selected = dataTemplate.SelectTemplate(orientation, page);
            page.Content = CreateItemView(orientation, selected);

            VisualStateManager.GoToState(page.Content, ToState(page.Width));
        }
    }

    private string GetOrientation(Page page)
    {
        return page.Width > page.Height ? "Landscape" : "Portrait";
    }

    private string ToState(double width)
    {
        if (width >= 1400)
            return "ExtraExtraLarge";
        if (width >= 1200)
            return "ExtraLarge";
        if (width >= 992)
            return "Large";
        if (width >= 768)
            return "Medium";
        if (width >= 576)
            return "Small";

        return "ExtraSmall";
    }
}

We can make the breakpoints settable via XAML or some other way to configure them, but you get the idea.

The AdaptableBehavior now does as our earlier example and responds to both size (breakpoints) as well as orientation. It uses the supplied DataTemplateSelector to choose the correct data template to use and then uses VSM to tell the template which size breakpoints to use.

If you run this example in Windows you’ll find it handles some different breakpoints but then when the breakpoint is not assigned any values the sizes will switch back to the defaults. As the AdaptableBehavior has no idea what’s listening to state changes, it’s difficult for it to handle this itself.

Ofcourse, there are other ways to achieve this, such as making the AdaptableBehavior the DataTemplateSelector (essentially like my Xamarin Forms OnOrientation code) and simply supply it with the various orientations and breakpoint UI templates. I’ll leave that to the reader to look into.

Is handling breakpoints using width good enough?

In much of this post we talk about breakpoints around the display’s width, but is just handling the MinWindowWidth good enough for our needs.

If we solely handle the width as our breakpoint trigger then we have a potential issue if the device switches to landscape mode. For example, let’s assume that we set the font size to 48 for small displays (upto 1200) and 96 for larger or equal to 1200 width. On a mobile phone in portrait the font may look perfect at 48, but when the phone is rotated to landscape mode the display is greater than 1200, switching to the larger font size which is now possible unusable.

We could obviously look at using triggers with breakpoints and then use orientation of device/platform to have alternate values or we could look to handle the height as a trigger as well – as you can see, this then starts to get complicated, requiring many visual states or triggers etc. for many device scenarios.

Ofcourse, we could fix the UI to portrait and make our lives simpler, but this will not suit all applications.

So, make sure you test your UI on different sizes devices and in different orientations.

There’s more…

Before we end this rather long post. I mentioned that the AdaptiveTrigger would (in MAUI 7.x) allow us to define XAML that’s triggered by breakpoint changes, there’s also the OrientationStateTrigger used like this

Update: I’ve just confirmed, MAUI 7.x has a working version of AdaptiveTrigger.

<VisualState.StateTriggers>
   <OrientationStateTrigger Orientation="Portrait" />
</VisualState.StateTriggers>

Is that it?

Our original aims were to be able to change things like font size, control sizes and layouts to be truly adaptive. Using DataTemplates with either triggers or behaviours allows us to achieve these goals, but there one last thing to look into/think about.

Not quite the same thing but MAUI controls come with the property FontAutoScalingEnabled set to true by default. Now this basically says, if the device scales the font, then our UI/control should respond accordingly and scale.

This is the option of some devices to change the Settings | Display | Font size. So when, like me, your eyesight’s not great on small devices, you tell the device to scale fonts to a larger size, FontAutoScalingEnabled=”True” means your font will get scaled, False means it will not. This is important to remember if you are already handling things like scaling your font size based upon breakpoints as it could affect your UI look if you max your font via the breakpoint and then find the user scales it further.

Let’s see this in actions. If we change our MainPage ContentPage to just having this XAML

<Grid>
   <Label Text="My AutoScaling Label"
      VerticalOptions="Center" 
      HorizontalOptions="Center"
      FontSize="32"
      FontAutoScalingEnabled="True" />
</Grid>

Now we run this up on an Android Phone emaulator, I’m using the Pixel 3 XL. What you should see is our text nicely display on a single line at the centre (vertically and horizontally) on the emulator. All looks pretty good.

Now go to the Android Settings | Display | Advanced section and select Font Size, you should see some sample text and a control to change the default text size – change this to the largest option, now return to your application UI and I was lucky, the label just fit on the one line, but as you can see, the font size increased and hence this might have an effect on your layout and design if you’ve already maximized your font size for the given screen size.

Hence, if you need to stop this happening, set FontAutoScalingEnabled=”False”

Code

Code is available via my blog-projects github repo..