diff --git a/Source/Meadow.Samples.sln b/Source/Meadow.Samples.sln
index 3c40401c..a4839ada 100644
--- a/Source/Meadow.Samples.sln
+++ b/Source/Meadow.Samples.sln
@@ -435,6 +435,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DesktopModbusClient", "Modb
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MeadowModbusServer", "Modbus\MeadowModbusServer\MeadowModbusServer.csproj", "{F6BA3311-6502-4A7E-89A9-D943245DCA00}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CellularSample", "ProjectLab\CellularSample\CellularSample.csproj", "{F7301799-6DAF-4A80-A034-0267DF5426AE}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -1916,6 +1918,15 @@ Global
{F6BA3311-6502-4A7E-89A9-D943245DCA00}.Simulation|Any CPU.ActiveCfg = Debug|Any CPU
{F6BA3311-6502-4A7E-89A9-D943245DCA00}.Simulation|Any CPU.Build.0 = Debug|Any CPU
{F6BA3311-6502-4A7E-89A9-D943245DCA00}.Simulation|Any CPU.Deploy.0 = Debug|Any CPU
+ {F7301799-6DAF-4A80-A034-0267DF5426AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F7301799-6DAF-4A80-A034-0267DF5426AE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F7301799-6DAF-4A80-A034-0267DF5426AE}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
+ {F7301799-6DAF-4A80-A034-0267DF5426AE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F7301799-6DAF-4A80-A034-0267DF5426AE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F7301799-6DAF-4A80-A034-0267DF5426AE}.Release|Any CPU.Deploy.0 = Release|Any CPU
+ {F7301799-6DAF-4A80-A034-0267DF5426AE}.Simulation|Any CPU.ActiveCfg = Debug|Any CPU
+ {F7301799-6DAF-4A80-A034-0267DF5426AE}.Simulation|Any CPU.Build.0 = Debug|Any CPU
+ {F7301799-6DAF-4A80-A034-0267DF5426AE}.Simulation|Any CPU.Deploy.0 = Debug|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -2123,6 +2134,7 @@ Global
{029D7D51-257B-4CA9-80AC-F8B0238B55DF} = {3B2ADC8C-1ACB-49F3-8A3C-4F453FF9FE75}
{4A4C3198-BF16-47DB-BA01-25DDCF77B2F5} = {3B2ADC8C-1ACB-49F3-8A3C-4F453FF9FE75}
{F6BA3311-6502-4A7E-89A9-D943245DCA00} = {3B2ADC8C-1ACB-49F3-8A3C-4F453FF9FE75}
+ {F7301799-6DAF-4A80-A034-0267DF5426AE} = {DA0CC626-D072-457F-89B7-C22427D4C775}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E3F002EA-1A25-487F-9A5D-93D1E7EC6E31}
diff --git a/Source/ProjectLab/CellularSample/.vscode/launch.json b/Source/ProjectLab/CellularSample/.vscode/launch.json
new file mode 100644
index 00000000..43067bdc
--- /dev/null
+++ b/Source/ProjectLab/CellularSample/.vscode/launch.json
@@ -0,0 +1,14 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Deploy",
+ "type": "meadow",
+ "request": "launch",
+ "preLaunchTask": "meadow: Build"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Source/ProjectLab/CellularSample/Assets/img-cell-0.bmp b/Source/ProjectLab/CellularSample/Assets/img-cell-0.bmp
new file mode 100644
index 00000000..94a07a65
Binary files /dev/null and b/Source/ProjectLab/CellularSample/Assets/img-cell-0.bmp differ
diff --git a/Source/ProjectLab/CellularSample/Assets/img-cell-1.bmp b/Source/ProjectLab/CellularSample/Assets/img-cell-1.bmp
new file mode 100644
index 00000000..28381671
Binary files /dev/null and b/Source/ProjectLab/CellularSample/Assets/img-cell-1.bmp differ
diff --git a/Source/ProjectLab/CellularSample/Assets/img-cell-2.bmp b/Source/ProjectLab/CellularSample/Assets/img-cell-2.bmp
new file mode 100644
index 00000000..66e655aa
Binary files /dev/null and b/Source/ProjectLab/CellularSample/Assets/img-cell-2.bmp differ
diff --git a/Source/ProjectLab/CellularSample/Assets/img-cell-3.bmp b/Source/ProjectLab/CellularSample/Assets/img-cell-3.bmp
new file mode 100644
index 00000000..06339e2b
Binary files /dev/null and b/Source/ProjectLab/CellularSample/Assets/img-cell-3.bmp differ
diff --git a/Source/ProjectLab/CellularSample/Assets/img-cell-4.bmp b/Source/ProjectLab/CellularSample/Assets/img-cell-4.bmp
new file mode 100644
index 00000000..e521dce3
Binary files /dev/null and b/Source/ProjectLab/CellularSample/Assets/img-cell-4.bmp differ
diff --git a/Source/ProjectLab/CellularSample/CellularSample.csproj b/Source/ProjectLab/CellularSample/CellularSample.csproj
new file mode 100644
index 00000000..53748901
--- /dev/null
+++ b/Source/ProjectLab/CellularSample/CellularSample.csproj
@@ -0,0 +1,41 @@
+
+
+ netstandard2.1
+ true
+ Library
+ App
+ 10.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+
\ No newline at end of file
diff --git a/Source/ProjectLab/CellularSample/DisplayController.cs b/Source/ProjectLab/CellularSample/DisplayController.cs
new file mode 100644
index 00000000..337d8a01
--- /dev/null
+++ b/Source/ProjectLab/CellularSample/DisplayController.cs
@@ -0,0 +1,94 @@
+using Meadow;
+using Meadow.Foundation.Graphics;
+using Meadow.Foundation.Graphics.MicroLayout;
+using Meadow.Peripherals.Displays;
+
+namespace CellularSample;
+
+public class DisplayController
+{
+ DisplayScreen screen;
+
+ private readonly Image imgSignal0Bar = Image.LoadFromResource("CellularSample.Assets.img-cell-0.bmp");
+ private readonly Image imgSignal1Bar = Image.LoadFromResource("CellularSample.Assets.img-cell-1.bmp");
+ private readonly Image imgSignal2Bar = Image.LoadFromResource("CellularSample.Assets.img-cell-2.bmp");
+ private readonly Image imgSignal3Bar = Image.LoadFromResource("CellularSample.Assets.img-cell-3.bmp");
+ private readonly Image imgSignal4Bar = Image.LoadFromResource("CellularSample.Assets.img-cell-4.bmp");
+
+ private Label status;
+ private Label ipAddress;
+ private Picture signalBars;
+
+ public DisplayController(IPixelDisplay _display)
+ {
+ screen = new DisplayScreen(_display, RotationType._270Degrees)
+ {
+ BackgroundColor = Color.FromHex("14607F")
+ };
+
+ screen.Controls.Add(new Box(5, 5, screen.Width - 10, screen.Height - 10)
+ {
+ IsFilled = false,
+ ForeColor = Color.FromHex("F9E000")
+ });
+
+ signalBars = new Picture(105, 33, 110, 103, imgSignal0Bar);
+ screen.Controls.Add(signalBars);
+
+ status = new Label(60, 149, 200, 24)
+ {
+ Text = "OFFLINE...",
+ TextColor = Color.FromHex("F9E000"),
+ Font = new Font16x24(),
+ HorizontalAlignment = HorizontalAlignment.Center,
+ };
+ screen.Controls.Add(status);
+
+ ipAddress = new Label(35, 183, 250, 28)
+ {
+ Text = "---.---.---.---",
+ TextColor = Color.FromHex("14607F"),
+ Font = new Font16x24(),
+ BackColor = Color.FromHex("F9E000"),
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Bottom,
+ };
+ screen.Controls.Add(ipAddress);
+ }
+
+ public void UpdateSignalBar(int strength)
+ {
+ Resolver.Log.Info("Signal Strength: " + strength);
+
+ switch (strength)
+ {
+ case int n when (n >= -70 && n <= -20):
+ signalBars.Image = imgSignal4Bar;
+ break;
+ case int n when (n >= -80 && n <= -71):
+ signalBars.Image = imgSignal3Bar;
+ break;
+ case int n when (n >= -90 && n <= -81):
+ signalBars.Image = imgSignal2Bar;
+ break;
+ case int n when (n >= -100 && n <= -91):
+ signalBars.Image = imgSignal1Bar;
+ break;
+ default:
+ signalBars.Image = imgSignal0Bar;
+ break;
+ }
+ }
+
+ public void UpdateStatus(string status)
+ {
+ this.status.Text = status;
+ }
+
+ public void UpdateIpAddress(string ipAddress)
+ {
+ this.ipAddress.Text = string.IsNullOrEmpty(ipAddress)
+ ? "---.---.---.---"
+ : ipAddress;
+ }
+}
\ No newline at end of file
diff --git a/Source/ProjectLab/CellularSample/MeadowApp.cs b/Source/ProjectLab/CellularSample/MeadowApp.cs
new file mode 100644
index 00000000..21e6e112
--- /dev/null
+++ b/Source/ProjectLab/CellularSample/MeadowApp.cs
@@ -0,0 +1,145 @@
+using Meadow;
+using Meadow.Devices;
+using Meadow.Foundation;
+using Meadow.Hardware;
+using System;
+using System.Diagnostics;
+using System.Net.Http;
+using System.Threading.Tasks;
+
+namespace CellularSample;
+
+public class MeadowApp : ProjectLabCoreComputeApp
+{
+ private DisplayController? displayController;
+
+ public override Task Initialize()
+ {
+ Resolver.Log.Info("Initialize...");
+
+ var cell = Hardware.ComputeModule.NetworkAdapters.Primary();
+ cell.NetworkConnected += CellAdapterNetworkConnected;
+ cell.NetworkConnecting += CellNetworkConnecting;
+ cell.NetworkDisconnected += CellAdapterNetworkDisconnected;
+ cell.NetworkConnectFailed += CellNetworkConnectFailed;
+
+ Resolver.Log.Info($"Running on ProjectLab Hardware {Hardware.RevisionString}");
+
+ if (Hardware.RgbLed is { } rgbLed)
+ {
+ rgbLed.SetColor(Color.Blue);
+ }
+
+ if (Hardware.Display is { } display)
+ {
+ displayController = new DisplayController(display);
+
+ displayController.UpdateSignalBar(cell.GetSignalQuality());
+ displayController.UpdateStatus(cell.IsConnected ? "CONNECTED" : "DISCONNECTED");
+ displayController.UpdateIpAddress(cell.IsConnected ? cell.IpAddress.ToString() : "---.---.---.---");
+ }
+
+ return Task.CompletedTask;
+ }
+
+ private async void CellAdapterNetworkConnected(INetworkAdapter networkAdapter, NetworkConnectionEventArgs e)
+ {
+ var cell = networkAdapter as ICellNetworkAdapter;
+
+ if (cell != null)
+ {
+ Resolver.Log.Info("Cell CSQ at the time of connection (dbm): " + cell.Csq);
+ Resolver.Log.Info("Cell IMEI: " + cell.Imei);
+
+ displayController.UpdateStatus("CONNECTED");
+ displayController.UpdateIpAddress(cell.IpAddress.ToString());
+ displayController.UpdateSignalBar(cell.Csq);
+
+ await GetWebPageViaHttpClient("https://postman-echo.com/get?fool=bar1&foo2=bar2");
+ }
+ }
+
+ private void CellNetworkConnecting(INetworkAdapter sender)
+ {
+ displayController.UpdateStatus("CONNECTING");
+ displayController.UpdateIpAddress("---.---.---.---");
+ displayController.UpdateSignalBar(-9999);
+ }
+
+ private void CellAdapterNetworkDisconnected(INetworkAdapter sender, NetworkDisconnectionEventArgs args)
+ {
+ displayController.UpdateStatus("DISCONNECTED");
+ displayController.UpdateIpAddress("---.---.---.---");
+ displayController.UpdateSignalBar(-9999);
+ }
+
+ private void CellNetworkConnectFailed(INetworkAdapter sender)
+ {
+ displayController.UpdateStatus("RECONNECT FAILED");
+ displayController.UpdateIpAddress("---.---.---.---");
+ displayController.UpdateSignalBar(-9999);
+ }
+
+ private async Task GetWebPageViaHttpClient(string uri)
+ {
+ Resolver.Log.Info($"Requesting {uri} - {DateTime.Now}");
+ Stopwatch stopwatch = new Stopwatch();
+ stopwatch.Start();
+
+ using (HttpClient client = new HttpClient())
+ {
+ // In weak signal connections and/or large download scenarios, it's recommended to increase the client timeout
+ client.Timeout = TimeSpan.FromMinutes(5);
+ using (HttpResponseMessage response = await client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead))
+ {
+ try
+ {
+ response.EnsureSuccessStatusCode();
+
+ var contentLength = response.Content.Headers.ContentLength ?? -1L;
+ var progress = new Progress(totalBytes =>
+ {
+ Resolver.Log.Info($"{totalBytes} bytes downloaded ({(double)totalBytes / contentLength:P2})");
+ });
+
+ using (var stream = await response.Content.ReadAsStreamAsync())
+ {
+ var buffer = new byte[4096];
+ long totalBytesRead = 0;
+ int bytesRead;
+
+ while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
+ {
+ totalBytesRead += bytesRead;
+ ((IProgress)progress).Report(totalBytesRead);
+ }
+ }
+
+ stopwatch.Stop();
+ Resolver.Log.Info($"Download complete. Time taken: {stopwatch.Elapsed.TotalSeconds:F2} seconds");
+ }
+ catch (TaskCanceledException)
+ {
+ Resolver.Log.Info("Request timed out.");
+ }
+ catch (Exception e)
+ {
+ Resolver.Log.Info($"Request went sideways: {e.Message}");
+ }
+ }
+ }
+ }
+
+ public override Task Run()
+ {
+ Resolver.Log.Info("Run...");
+
+ if (Hardware?.RgbLed is { } rgbLed)
+ {
+ Resolver.Log.Info("starting blink");
+ _ = rgbLed.StartBlink(WildernessLabsColors.PearGreen, TimeSpan.FromMilliseconds(500), TimeSpan.FromMilliseconds(2000), 0.5f);
+ }
+
+ return Task.CompletedTask;
+ }
+}
\ No newline at end of file
diff --git a/Source/ProjectLab/CellularSample/app.build.yaml b/Source/ProjectLab/CellularSample/app.build.yaml
new file mode 100644
index 00000000..7b9be619
--- /dev/null
+++ b/Source/ProjectLab/CellularSample/app.build.yaml
@@ -0,0 +1,2 @@
+Deploy:
+ NoLink: [ ProjectLab ]
\ No newline at end of file
diff --git a/Source/ProjectLab/CellularSample/app.config.yaml b/Source/ProjectLab/CellularSample/app.config.yaml
new file mode 100644
index 00000000..137b4dc5
--- /dev/null
+++ b/Source/ProjectLab/CellularSample/app.config.yaml
@@ -0,0 +1,36 @@
+# Uncomment additional options as needed.
+# To learn more about these config options, including custom application configuration settings, check out the Application Settings Configuration documentation.
+# http://developer.wildernesslabs.co/Meadow/Meadow.OS/Configuration/Application_Settings_Configuration/
+
+# App lifecycle configuration.
+Lifecycle:
+
+ # Control whether Meadow will restart when an unhandled app exception occurs. Combine with Lifecycle > AppFailureRestartDelaySeconds to control restart timing.
+ RestartOnAppFailure: true
+
+ # When app set to restart automatically on app failure,
+# AppFailureRestartDelaySeconds: 15
+
+# Logging configuration.
+Logging:
+
+ # Adjust the level of logging detail.
+ LogLevel:
+
+ # Trace, Debug, Information, Warning, or Error
+ Default: Trace
+
+# Meadow.Cloud configuration.
+#MeadowCloud:
+
+ # Enable Logging, Events, Command + Control
+# Enabled: false
+
+ # Enable Over-the-air Updates
+# EnableUpdates: false
+
+ # Enable Health Metrics
+# EnableHealthMetrics: false
+
+ # How often to send metrics to Meadow.Cloud
+# HealthMetricsIntervalMinutes: 60
\ No newline at end of file
diff --git a/Source/ProjectLab/CellularSample/cell.config.yaml b/Source/ProjectLab/CellularSample/cell.config.yaml
new file mode 100644
index 00000000..25657f7a
--- /dev/null
+++ b/Source/ProjectLab/CellularSample/cell.config.yaml
@@ -0,0 +1,5 @@
+Settings:
+ APN: teal # (required) Access Point Name
+ Module: BG95M3 # (required) Module model (BG770A, BG95M3 or M95)
+ Interface: COM1 # (required) Serial interface (COM1 (default), COM4 or COM6)
+ EnablePin: A3 # (required) Enable MCU pin to turn the module on/off.
\ No newline at end of file
diff --git a/Source/ProjectLab/CellularSample/meadow.config.yaml b/Source/ProjectLab/CellularSample/meadow.config.yaml
new file mode 100644
index 00000000..afebdca7
--- /dev/null
+++ b/Source/ProjectLab/CellularSample/meadow.config.yaml
@@ -0,0 +1,39 @@
+# Acceptable values for true: true, 1, yes
+# Acceptable values for false: false, 0, no
+
+# Main Device Config
+Device:
+
+ # Name of the device on the network.
+ Name: MeadowDevice
+
+ # Corresponding MCU pin names for the reserved pins
+ # (COMX_RX pin, COM_TX pin, ENABLE pin)
+ # Examples:
+ # Using mikroBUS 1 on Project Lab v3 with Quectel BG95-M3 (Meadow AN pin as enable pin),
+ # reserve the following:
+ ReservedPins: B15;B14;A3
+
+# Network configuration.
+Network:
+
+ # Which interface should be used?
+ DefaultInterface: Cell
+
+ # Automatically attempt to get the time at startup?
+ GetNetworkTimeAtStartup: true
+
+ # Time synchronization period in seconds.
+ NtpRefreshPeriod: 600
+
+ # Name of the NTP servers.
+ NtpServers:
+ - 0.pool.ntp.org
+ - 1.pool.ntp.org
+ - 2.pool.ntp.org
+ - 3.pool.ntp.org
+
+ # IP addresses of the DNS servers.
+ DnsServers:
+ - 1.1.1.1
+ - 8.8.8.8
\ No newline at end of file