Project updated to v1.0 (released on May 18th, 2023)
In this introductory project we're going to learn how simple is to control SG90 Micro Servo connected to a Meadow board with a .NET MAUI app using Bluetooth!
Meadow.Foundation a platform for quickly and easily building connected things using.NET on Meadow. Created by Wilderness Labs, It's completely open source and maintained by the Wilderness Labs community.
If you're new working with Meadow, I suggest you go to the Getting Started w/ Meadow by Controlling the Onboard RGB LED project to properly set up your development environment, including flashing your board to the latest version of MeadowOS.
Step 1 - Assemble the circuitWire your project like this:
Create a new Meadow Application project in Visual Studio 2019 for Windows or macOS and name it MeadowBleServo.
Step 3 - Add the required NuGet packagesFor this project, search and install the following NuGet packages:
Step 4 - Write the code for MeadowBleServoServoController.cs
Add the following class to your project:
public class ServoController
{
private static readonly Lazy<ServoController> instance =
new Lazy<ServoController>(() => new ServoController());
public static ServoController Instance => instance.Value;
Servo servo;
CancellationTokenSource cancellationTokenSource = null;
int _rotationAngle;
private ServoController()
{
Initialize();
}
public void Initialize()
{
servo = new Servo(MeadowApp.Device, MeadowApp.Device.Pins.D10, NamedServoConfigs.SG90);
servo.RotateTo(NamedServoConfigs.SG90.MinimumAngle);
}
public void RotateTo(int angle)
{
servo.RotateTo(new Angle(angle));
}
public void StopSweep()
{
cancellationTokenSource?.Cancel();
}
public void StartSweep()
{
cancellationTokenSource = new CancellationTokenSource();
Task.Run(async () =>
{
await StartSweep(cancellationTokenSource.Token);
},
cancellationTokenSource.Token);
}
async Task StartSweep(CancellationToken cancellationToken)
{
Console.WriteLine("Sweeping");
while (true)
{
if (cancellationToken.IsCancellationRequested) { break; }
while (_rotationAngle < 180)
{
if (cancellationToken.IsCancellationRequested) { break; }
_rotationAngle++;
servo.RotateTo(new Angle(_rotationAngle, AU.Degrees));
await Task.Delay(50);
}
while (_rotationAngle > 0)
{
if (cancellationToken.IsCancellationRequested) { break; }
_rotationAngle--;
servo.RotateTo(new Angle(_rotationAngle, AU.Degrees));
await Task.Delay(50);
}
}
}
}
This singleton class is used to encapsulate the logic of what we intend to do with a Servo and also make it accessible from our main MeadowApp class for when we start receiving commands via Bluetooth:
RotateTo
- Rotate the servo to a specific angle.StopSweep
- Stops the servo from rotating back and forth at 50ms per angle.StartSweep
- Starts the servo to rotate back and forth at 50ms per angle.
MeadowApp.cs
In your MeadowApp class, copy the following code:
public class MeadowApp : App<F7FeatherV2>
{
Definition bleTreeDefinition;
CharacteristicBool isSweepingCharacteristic;
CharacteristicInt32 angleCharacteristic;
readonly string IS_SWEEPING = "24517ccc888e4ffc9da521884353b08d";
readonly string ANGLE = "5a0bb01669ab4a49a2f2de5b292458f3";
public override Task Initialize()
{
var onboardLed = new RgbPwmLed(
device: Device,
redPwmPin: Device.Pins.OnboardLedRed,
greenPwmPin: Device.Pins.OnboardLedGreen,
bluePwmPin: Device.Pins.OnboardLedBlue);
onboardLed.SetColor(Color.Red);
ServoController.Instance.Initialize();
bleTreeDefinition = GetDefinition();
Device.BluetoothAdapter.StartBluetoothServer(bleTreeDefinition);
isSweepingCharacteristic.ValueSet += IsSweepingCharacteristicValueSet;
angleCharacteristic.ValueSet += AngleCharacteristicValueSet;
onboardLed.SetColor(Color.Green);
return base.Initialize();
}
void IsSweepingCharacteristicValueSet(ICharacteristic c, object data)
{
if ((bool)data)
{
ServoController.Instance.StopSweep();
isSweepingCharacteristic.SetValue(false);
}
else
{
ServoController.Instance.StartSweep();
isSweepingCharacteristic.SetValue(true);
}
}
void AngleCharacteristicValueSet(ICharacteristic c, object data)
{
int angle = (int)data;
ServoController.Instance.RotateTo(angle);
}
Definition GetDefinition()
{
isSweepingCharacteristic = new CharacteristicBool(
name: "IsSweeping",
uuid: IS_SWEEPING,
permissions: CharacteristicPermission.Read | CharacteristicPermission.Write,
properties: CharacteristicProperty.Read | CharacteristicProperty.Write);
angleCharacteristic = new CharacteristicInt32(
name: "Angle",
uuid: ANGLE,
permissions: CharacteristicPermission.Read | CharacteristicPermission.Write,
properties: CharacteristicProperty.Read | CharacteristicProperty.Write);
var service = new Service(
name: "ServiceA",
uuid: 253,
isSweepingCharacteristic,
angleCharacteristic
);
return new Definition("MeadowServo", service);
}
}
In the Initialize
method, we create a RgbPwmLed
object which we set color Red to indicate that the app has started. We then call our ServoController
singleton calling the Initialize method, which will turn the servo to its 0 degree position. After that we build our Bluetooth definition tree that we pass in when starting the Bluetooth server. We create event handlers for the two characteristic we're exposing here:
isSweepingCharacteristic
- a boolean with read/write permissions to turn start or stop the sweeping animation where the servo rotates from 0 to 180 degrees back and forth.angleCharacteristic
- a 32 bit integer with read/write permissions to set the rotate the servo at a specific angle.
Finally, the RGB LED is turned to green to indicate that the app has finished initializing and its discoverable to the Bluetooth mobile app.
Step 5 - .NET MAUI Bluetooth AppAs we mentioned, in this project we included a .NET MAUI that runs on iOS and Android.
The basic things we need to understand here are:
If you check their official GitHub docs, you'll see how to add the required permissions in the Android's manifest file and iOS's info.plist file, how to scan for devices and handle the event of discovered devices.
If you check the BaseViewModel
class, in the AdapterDeviceDiscovered
event handler we have:
async void AdapterDeviceDiscovered(object sender, DeviceEventArgs e)
{
if (DeviceList.FirstOrDefault(x => x.Name == e.Device.Name) == null &&
!string.IsNullOrEmpty(e.Device.Name))
{
DeviceList.Add(e.Device);
}
if (e != null &&
e.Device != null &&
!string.IsNullOrEmpty(e.Device.Name) &&
e.Device.Name.Contains("Meadow"))
{
await adapter.StopScanningForDevicesAsync();
IsDeviceListEmpty = false;
DeviceSelected = e.Device;
}
}
In every device discovered, if its partially named Meadow, it'll be set as DeviceSelected
, which you'll see it selected automatically in the device Picker.
Connecting/Disconnecting to Meadow
Once MeadowServo
is selected, you can now tap on the connect/disconnect toggle button on the right next to the search for devices button.
async Task ToggleConnection()
{
try
{
if (IsConnected)
{
await adapter.DisconnectDeviceAsync(DeviceSelected);
IsConnected = false;
}
else
{
await adapter.ConnectToDeviceAsync(DeviceSelected);
IsConnected = true;
}
}
catch (DeviceConnectionException ex)
{
Debug.WriteLine(ex.Message);
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
Once connected to Meadow, IsConnected property becomes true
, and you'll see the connected icon, and the you can use the slider to rotate the servo at a specific degree or start a sweeping animation.
When running both the MAUI app and Meadow app, once connected successfully, you should be able to rotate the servo at a certain degree with the slider or start/stop a sweeping animation. It should look like to the following GIF:
This project is only the tip of the iceberg in terms of the extensive exciting things you can do with Meadow.Foundation.
- It comes with a huge peripheral driver library with drivers for the most common sensors and peripherals.
- The peripheral drivers encapsulate the core logic and expose a simple, clean, modern API.
- This project is backed by a growing community that is constantly working on building cool connected things and are always excited to help new-comers and discuss new projects.
Comments