diff --git a/devices/LoRa/Datasheet/60852689.DS_SX1261_2 V2-2.pdf b/devices/LoRa/Datasheet/60852689.DS_SX1261_2 V2-2.pdf
new file mode 100644
index 0000000000..45c6fb0376
Binary files /dev/null and b/devices/LoRa/Datasheet/60852689.DS_SX1261_2 V2-2.pdf differ
diff --git a/devices/LoRa/LoRa.nuspec b/devices/LoRa/LoRa.nuspec
new file mode 100644
index 0000000000..7dc3977a2b
--- /dev/null
+++ b/devices/LoRa/LoRa.nuspec
@@ -0,0 +1,39 @@
+
+
+
+ nanoFramework.Iot.Device.LoRa
+ $version$
+ nanoFramework.Iot.Device.LoRa
+ nanoframework
+ false
+ LICENSE.md
+
+
+ docs\README.md
+ false
+ https://github.com/nanoframework/nanoFramework.IoT.Device
+ images\nf-logo.png
+
+ Copyright (c) .NET Foundation and Contributors
+ This package includes the LoRa binding Iot.Device.LoRa for .NET nanoFramework C# projects.
+ Iot.Device.LoRa assembly for .NET nanoFramework C# projects
+ nanoFramework C# csharp netmf netnf Iot.Device.LoRa LoRa SX1262
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/devices/LoRa/LoRa.sln b/devices/LoRa/LoRa.sln
new file mode 100644
index 0000000000..21fe1492f3
--- /dev/null
+++ b/devices/LoRa/LoRa.sln
@@ -0,0 +1,57 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.14.37111.16
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{11A8DD76-328B-46DF-9F39-F559912D0360}") = "LoRa", "LoRa\LoRa.nfproj", "{5D8A46C7-F705-4AAB-AF5E-D14657161340}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
+EndProject
+Project("{11A8DD76-328B-46DF-9F39-F559912D0360}") = "Sx1262Sample", "samples\Sx1262Sample\Sx1262Sample.nfproj", "{E4A64F77-59D4-4A2F-B0FE-8315ED264D89}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{A0CB0C00-272F-4976-9DC2-B305E2DF5221}"
+EndProject
+Project("{11A8DD76-328B-46DF-9F39-F559912D0360}") = "LoRaTests", "tests\LoRaTests.nfproj", "{DA6342A6-C455-450D-9921-CC76A0032F1D}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}"
+ ProjectSection(SolutionItems) = preProject
+ category.txt = category.txt
+ README.md = README.md
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {5D8A46C7-F705-4AAB-AF5E-D14657161340}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5D8A46C7-F705-4AAB-AF5E-D14657161340}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5D8A46C7-F705-4AAB-AF5E-D14657161340}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
+ {5D8A46C7-F705-4AAB-AF5E-D14657161340}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5D8A46C7-F705-4AAB-AF5E-D14657161340}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5D8A46C7-F705-4AAB-AF5E-D14657161340}.Release|Any CPU.Deploy.0 = Release|Any CPU
+ {E4A64F77-59D4-4A2F-B0FE-8315ED264D89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E4A64F77-59D4-4A2F-B0FE-8315ED264D89}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E4A64F77-59D4-4A2F-B0FE-8315ED264D89}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
+ {E4A64F77-59D4-4A2F-B0FE-8315ED264D89}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E4A64F77-59D4-4A2F-B0FE-8315ED264D89}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E4A64F77-59D4-4A2F-B0FE-8315ED264D89}.Release|Any CPU.Deploy.0 = Release|Any CPU
+ {DA6342A6-C455-450D-9921-CC76A0032F1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DA6342A6-C455-450D-9921-CC76A0032F1D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DA6342A6-C455-450D-9921-CC76A0032F1D}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
+ {DA6342A6-C455-450D-9921-CC76A0032F1D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DA6342A6-C455-450D-9921-CC76A0032F1D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DA6342A6-C455-450D-9921-CC76A0032F1D}.Release|Any CPU.Deploy.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {E4A64F77-59D4-4A2F-B0FE-8315ED264D89} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
+ {DA6342A6-C455-450D-9921-CC76A0032F1D} = {A0CB0C00-272F-4976-9DC2-B305E2DF5221}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {0ADD658D-0671-4036-BAA4-7A6C246D8E66}
+ EndGlobalSection
+EndGlobal
diff --git a/devices/LoRa/LoRa/Drivers/Sx1262.cs b/devices/LoRa/LoRa/Drivers/Sx1262.cs
new file mode 100644
index 0000000000..9f72fc390e
--- /dev/null
+++ b/devices/LoRa/LoRa/Drivers/Sx1262.cs
@@ -0,0 +1,716 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Buffers.Binary;
+using System.Device.Gpio;
+using System.Device.Spi;
+using System.Diagnostics;
+using System.Threading;
+
+using Iot.Device.LoRa;
+
+namespace Iot.Device.LoRa.Drivers.Sx1262
+{
+ ///
+ /// Low-level SX1262 LoRa radio driver.
+ /// Heltec Vision Master E213 (HT-VME213) default pin mapping: NSS = 8, SCK = 9, MOSI = 10, MISO = 11, RST = 12, BUSY = 13, DIO1 = 14.
+ /// Supports reset, initialization, TX, RX polling, and buffer access.
+ /// https://semtech.my.salesforce.com/sfc/p/#E0000000JelG/a/RQ000008n3pp/qXjWn19TZmb.1MgqPZ8Vrc5V7U.M_lOAIoTZHcEAeTI or see datasheet folder.
+ ///
+ public class Sx1262 : ILoRaDevice
+ {
+ ///
+ /// Maximum TX/RX payload length supported by this driver (matches the SX1262 length field in this configuration).
+ ///
+ public const int MaxPayloadLength = 255;
+
+ // ---------------------------------------------------------------
+ // Op-codes (datasheet section 11.1)
+ // ---------------------------------------------------------------
+ private const byte OpGetStatus = 0xC0;
+ private const byte OpSetStandby = 0x80;
+ private const byte OpSetSleep = 0x84;
+ private const byte OpSetPacketType = 0x8A;
+ private const byte OpSetRfFrequency = 0x86;
+ private const byte OpSetTxParams = 0x8E;
+ private const byte OpSetPaConfig = 0x95;
+ private const byte OpSetModulationParams = 0x8B;
+ private const byte OpSetPacketParams = 0x8C;
+ private const byte OpSetBufferBaseAddr = 0x98;
+ private const byte OpSetDioIrqParams = 0x08;
+ private const byte OpGetIrqStatus = 0x12;
+ private const byte OpClearIrqStatus = 0x02;
+ private const byte OpSetRx = 0x82;
+ private const byte OpSetTx = 0x83;
+ private const byte OpWriteBuffer = 0x0E;
+ private const byte OpReadBuffer = 0x1E;
+ private const byte OpGetRxBufferStatus = 0x13;
+ private const byte OpGetPacketStatus = 0x14;
+ private const byte OpSetDio3AsTcxoCtrl = 0x97;
+ private const byte OpSetDio2AsRfSwCtrl = 0x9D;
+ private const byte OpSetRegulatorMode = 0x96;
+ private const byte OpCalibrate = 0x89;
+
+ // ---------------------------------------------------------------
+ // IRQ bit masks (datasheet section 13.3.2)
+ // ---------------------------------------------------------------
+ private const ushort IrqTxDone = 0x0001;
+ private const ushort IrqRxDone = 0x0002;
+ private const ushort IrqCrcErr = 0x0040;
+ private const ushort IrqTimeout = 0x0200;
+
+ // ---------------------------------------------------------------
+ // Hardware
+ // ---------------------------------------------------------------
+ private readonly SpiDevice _spi;
+ private readonly GpioController _gpio;
+ private readonly bool _shouldDispose;
+ private readonly bool _disposeSpi;
+ private readonly object _sendLock = new object();
+ private readonly object _pollLock = new object();
+
+ private GpioPin _resetPin;
+ private GpioPin _busyPin;
+ private GpioPin _dio1Pin;
+
+ private bool _disposed;
+
+ // ---------------------------------------------------------------
+ // RX poll thread
+ // ---------------------------------------------------------------
+ private Thread _pollThread;
+
+ // 0 = poll loop runs; 1 = stop requested. Use Interlocked for cross-thread visibility (documented on nanoFramework).
+ private int _stopPolling;
+
+ // ---------------------------------------------------------------
+ // Static helpers (SA1204: public static before non-static public)
+ //// ---------------------------------------------------------------
+
+ ///
+ /// Decodes chip mode bits [6:4] from a raw status byte.
+ ///
+ /// The status byte returned by the chip.
+ /// A short label describing the chip mode.
+ public static string DecodeChipMode(byte status)
+ {
+ byte mode = (byte)((status >> 4) & 0x07);
+ switch (mode)
+ {
+ case 0x02: return "STDBY_RC";
+ case 0x03: return "STDBY_XOSC";
+ case 0x04: return "FS";
+ case 0x05: return "RX";
+ case 0x06: return "TX";
+ default: return "UNKNOWN";
+ }
+ }
+
+ // ---------------------------------------------------------------
+ // Construction
+ //// ---------------------------------------------------------------
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The SPI device for the radio.
+ /// GPIO pin number for reset (active low).
+ /// GPIO pin number for the BUSY line.
+ /// GPIO pin number for DIO1 (IRQ).
+ /// Optional shared GPIO controller; a new instance is created when null.
+ /// True to dispose the GPIO controller when this instance is disposed.
+ /// True to dispose when this instance is disposed (use false when the bus is shared).
+ ///
+ /// GPIO pins are opened incrementally. Temporary references hold partially opened pins; after all opens succeed, fields are assigned and temporaries are nulled so failure cleanup only disposes pins that were actually opened.
+ ///
+ public Sx1262(
+ SpiDevice spiDevice,
+ int resetPin,
+ int busyPin,
+ int dio1Pin,
+ GpioController gpioController = null,
+ bool shouldDispose = true,
+ bool disposeSpi = false)
+ {
+ if (spiDevice == null)
+ {
+ throw new ArgumentNullException(nameof(spiDevice));
+ }
+
+ if (resetPin < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(resetPin));
+ }
+
+ if (busyPin < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(busyPin));
+ }
+
+ if (dio1Pin < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(dio1Pin));
+ }
+
+ _spi = spiDevice;
+ _gpio = gpioController == null ? new GpioController() : gpioController;
+ _shouldDispose = shouldDispose || gpioController == null;
+ _disposeSpi = disposeSpi;
+
+ GpioPin resetPinObj = null;
+ GpioPin busyPinObj = null;
+ GpioPin dio1PinObj = null;
+ try
+ {
+ resetPinObj = _gpio.OpenPin(resetPin, PinMode.Output);
+ busyPinObj = _gpio.OpenPin(busyPin, PinMode.Input);
+ dio1PinObj = _gpio.OpenPin(dio1Pin, PinMode.Input);
+ resetPinObj.Write(PinValue.High);
+ _resetPin = resetPinObj;
+ _busyPin = busyPinObj;
+ _dio1Pin = dio1PinObj;
+ resetPinObj = null;
+ busyPinObj = null;
+ dio1PinObj = null;
+ }
+ catch
+ {
+ if (dio1PinObj != null)
+ {
+ dio1PinObj.Dispose();
+ }
+
+ if (busyPinObj != null)
+ {
+ busyPinObj.Dispose();
+ }
+
+ if (resetPinObj != null)
+ {
+ resetPinObj.Dispose();
+ }
+
+ if (_shouldDispose && _gpio != null)
+ {
+ _gpio.Dispose();
+ }
+
+ throw;
+ }
+ }
+
+ // ---------------------------------------------------------------
+ // Events
+ //// ---------------------------------------------------------------
+
+ ///
+ public event PacketReceivedHandler PacketReceived;
+
+ // ---------------------------------------------------------------
+ // Step 1 — Reset + BUSY + GetStatus
+ //// ---------------------------------------------------------------
+
+ ///
+ public void Reset()
+ {
+ _resetPin.Write(PinValue.High);
+ Thread.Sleep(10);
+ _resetPin.Write(PinValue.Low);
+ Thread.Sleep(100);
+ _resetPin.Write(PinValue.High);
+ Thread.Sleep(10);
+ WaitBusy(5000);
+ }
+
+ ///
+ /// Blocks until BUSY goes low or the timeout expires.
+ ///
+ /// Maximum time to wait, in milliseconds.
+ /// BUSY did not go low within .
+ public void WaitBusy(int timeoutMs)
+ {
+ int elapsed = 0;
+ while (_busyPin.Read() == PinValue.High)
+ {
+ Thread.Sleep(1);
+ if (++elapsed >= timeoutMs)
+ {
+ throw new TimeoutException();
+ }
+ }
+ }
+
+ ///
+ /// Reads the chip status byte.
+ ///
+ /// The second byte of the status SPI transaction.
+ public byte GetStatus()
+ {
+ byte[] tx = new byte[] { OpGetStatus, 0x00 };
+ byte[] rx = new byte[2];
+ WaitBusy(5000);
+ _spi.TransferFullDuplex(tx, rx);
+ return rx[1];
+ }
+
+ // ---------------------------------------------------------------
+ // Step 2 — Full init sequence
+ //// ---------------------------------------------------------------
+
+ ///
+ ///
+ /// Command opcodes are listed in the Semtech SX1261/2 datasheet §11.1.
+ /// Literal parameter bytes below follow §13.4 (configuration): TCXO on DIO3, calibration, regulator, standby, packet type, PA, modulation, IRQ mapping, etc. Adjust values for your board and RF plan; this sequence matches a typical 868 MHz LoRa setup.
+ ///
+ public void Initialize()
+ {
+ WriteCommand(OpSetDio3AsTcxoCtrl, new byte[] { 0x02, 0x00, 0x01, 0x40 });
+ WriteCommand(OpCalibrate, new byte[] { 0x7F });
+ WaitBusy(3000);
+ WriteCommand(OpSetDio2AsRfSwCtrl, new byte[] { 0x01 });
+ WriteCommand(OpSetRegulatorMode, new byte[] { 0x01 });
+ WriteCommand(OpSetStandby, new byte[] { 0x01 });
+ WriteCommand(OpSetPacketType, new byte[] { 0x01 });
+ SetRfFrequency(868000000);
+ WriteCommand(OpSetPaConfig, new byte[] { 0x04, 0x07, 0x00, 0x01 });
+ WriteCommand(OpSetTxParams, new byte[] { 0x0E, 0x04 });
+ WriteCommand(OpSetModulationParams, new byte[] { 0x07, 0x04, 0x01, 0x00 });
+ WriteCommand(OpSetPacketParams, new byte[] { 0x00, 0x08, 0x00, 0xFF, 0x01, 0x00 });
+ WriteCommand(OpSetBufferBaseAddr, new byte[] { 0x00, 0x00 });
+ byte[] irqParams = new byte[] { 0x02, 0x03, 0x02, 0x03, 0x00, 0x00, 0x00, 0x00 };
+ WriteCommand(OpSetDioIrqParams, irqParams);
+ }
+
+ ///
+ public void SetRfFrequency(uint frequencyHz)
+ {
+ ulong frf = ((ulong)frequencyHz << 25) / 32000000UL;
+ byte[] rfFreqBytes = new byte[4];
+ BinaryPrimitives.WriteUInt32BigEndian(rfFreqBytes, (uint)frf);
+ WriteCommand(OpSetRfFrequency, rfFreqBytes);
+ }
+
+ ///
+ /// Puts the chip into continuous RX mode (timeout = 0xFFFFFF).
+ ///
+ /// Called automatically by and after transmit.
+ public void StartReceiving()
+ {
+ WriteCommand(OpSetRx, new byte[] { 0xFF, 0xFF, 0xFF });
+ }
+
+ // ---------------------------------------------------------------
+ // Step 3 — TX
+ //// ---------------------------------------------------------------
+
+ ///
+ /// This method was called from the RX polling thread (e.g. inside ). Post work to another thread instead.
+ /// DIO1 did not indicate TX completion within , or the chip reported a TX timeout IRQ.
+ public void Send(byte[] payload, int timeoutMs)
+ {
+ // Poll-thread check must run before _sendLock: the poll thread must never block on _sendLock
+ // (e.g. if a PacketReceived handler calls Send), or it can deadlock with the thread holding the lock.
+ bool wasPolling;
+ lock (_pollLock)
+ {
+ if (_pollThread != null && Thread.CurrentThread == _pollThread)
+ {
+ throw new InvalidOperationException();
+ }
+
+ wasPolling = _pollThread != null;
+ }
+
+ // Never call StartPolling while holding _sendLock: the poll thread may invoke PacketReceived
+ // and a handler could call Send again, which would deadlock. Restart RX only after releasing the lock.
+ bool restoreRxAfterSend = false;
+ lock (_sendLock)
+ {
+ SendCore(payload, timeoutMs, wasPolling, out restoreRxAfterSend);
+ }
+
+ if (restoreRxAfterSend)
+ {
+ StartPolling();
+ }
+ }
+
+ /// TX path shared by .
+ /// The payload to send.
+ /// The timeout in milliseconds.
+ /// Indicates whether polling was active before sending.
+ /// Set to true to restore RX polling after sending.
+ /// is null.
+ /// is empty.
+ /// is longer than .
+ /// is not positive.
+ /// TX did not complete in time or the chip signaled a TX timeout.
+ /// The chip did not report TxDone after TX.
+ private void SendCore(byte[] payload, int timeoutMs, bool wasPolling, out bool restoreRxAfterSend)
+ {
+ restoreRxAfterSend = false;
+
+ if (payload == null)
+ {
+ throw new ArgumentNullException(nameof(payload));
+ }
+
+ if (payload.Length == 0)
+ {
+ throw new ArgumentException(string.Empty, nameof(payload));
+ }
+
+ if (payload.Length > MaxPayloadLength)
+ {
+ throw new ArgumentOutOfRangeException(nameof(payload));
+ }
+
+ if (timeoutMs <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(timeoutMs));
+ }
+
+ if (wasPolling)
+ {
+ StopPolling();
+ restoreRxAfterSend = true;
+ }
+
+ ClearIrqStatus(0xFFFF);
+
+ byte[] packetParams = new byte[]
+ {
+ 0x00, 0x08, 0x00,
+ (byte)payload.Length,
+ 0x01, 0x00
+ };
+ WriteCommand(OpSetPacketParams, packetParams);
+
+ WriteBuffer(0x00, payload);
+ WriteCommand(OpSetTx, new byte[] { 0x00, 0x00, 0x00 });
+
+ int elapsed = 0;
+ while (!IsDio1High)
+ {
+ Thread.Sleep(1);
+ if (++elapsed >= timeoutMs)
+ {
+ throw new TimeoutException();
+ }
+ }
+
+ ushort irq = GetIrqStatus();
+ ClearIrqStatus(0xFFFF);
+
+ if ((irq & IrqTimeout) != 0)
+ {
+ throw new TimeoutException();
+ }
+
+ if ((irq & IrqTxDone) == 0)
+ {
+ throw new InvalidOperationException();
+ }
+ }
+
+ ///
+ /// Writes bytes into the SX1262 data buffer at the given offset.
+ ///
+ /// Start offset in the chip buffer.
+ /// Payload bytes to write.
+ public void WriteBuffer(byte offset, byte[] data)
+ {
+ byte[] tx = new byte[2 + data.Length];
+ tx[0] = OpWriteBuffer;
+ tx[1] = offset;
+ Array.Copy(data, 0, tx, 2, data.Length);
+ WaitBusy(5000);
+ _spi.Write(tx);
+ }
+
+ // ---------------------------------------------------------------
+ // Step 4 — RX
+ //// ---------------------------------------------------------------
+
+ ///
+ /// Gets the RX buffer status after RxDone fires on DIO1.
+ ///
+ /// Receives the length of the received payload.
+ /// Receives the buffer offset of the payload.
+ public void GetRxBufferStatus(out byte payloadLength, out byte bufferOffset)
+ {
+ byte[] r = ReadCommand(OpGetRxBufferStatus, 2);
+ payloadLength = r[0];
+ bufferOffset = r[1];
+ }
+
+ ///
+ /// Reads bytes from the chip RX buffer starting at .
+ ///
+ /// Offset in the RX buffer.
+ /// Number of bytes to read.
+ /// A copy of the received bytes.
+ public byte[] ReadBuffer(byte offset, byte length)
+ {
+ byte[] tx = new byte[3 + length];
+ byte[] rx = new byte[3 + length];
+ tx[0] = OpReadBuffer;
+ tx[1] = offset;
+ WaitBusy(5000);
+ _spi.TransferFullDuplex(tx, rx);
+ byte[] result = new byte[length];
+ Array.Copy(rx, 3, result, 0, length);
+ return result;
+ }
+
+ ///
+ /// Gets the signal quality for the last received packet.
+ ///
+ /// Receives RSSI in dBm.
+ /// Receives SNR in dB.
+ public void GetPacketStatus(out int rssi, out float snr)
+ {
+ byte[] r = ReadCommand(OpGetPacketStatus, 3);
+ rssi = -(r[0] / 2);
+ snr = ((sbyte)r[1]) / 4.0f;
+ }
+
+ ///
+ /// Reads IRQ flags, pulls the packet from the buffer, raises , then returns to RX mode.
+ ///
+ /// A on success; null on CRC error or timeout.
+ public LoRaMessage HandleRxDone()
+ {
+ ushort irq = GetIrqStatus();
+ ClearIrqStatus(0xFFFF);
+
+ LoRaMessage msg = null;
+ try
+ {
+ if ((irq & IrqTimeout) != 0)
+ {
+ return null;
+ }
+
+ if ((irq & IrqCrcErr) != 0)
+ {
+ return null;
+ }
+
+ if ((irq & IrqRxDone) == 0)
+ {
+ return null;
+ }
+
+ GetRxBufferStatus(out byte length, out byte offset);
+ byte[] payload = ReadBuffer(offset, length);
+
+ GetPacketStatus(out int rssi, out float snr);
+ msg = new LoRaMessage(payload, rssi, snr);
+ }
+ finally
+ {
+ StartReceiving();
+ }
+
+ if (msg != null)
+ {
+ PacketReceivedHandler handler = PacketReceived;
+ if (handler != null)
+ {
+ try
+ {
+ handler(this, msg);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine("LoRa PacketReceived handler failed: " + ex.Message);
+ }
+ }
+ }
+
+ return msg;
+ }
+
+ // ---------------------------------------------------------------
+ // RX poll thread (nanoFramework safe)
+ //// ---------------------------------------------------------------
+
+ ///
+ public void StartPolling()
+ {
+ // Keep new Thread(), Start(), and _pollThread assignment under the same lock as StopPolling uses
+ // so another thread never Join()s a worker that is published but not yet started.
+ // Callers must not invoke Send from PacketReceived (poll thread); doing so would deadlock while this lock is held during Start().
+ lock (_pollLock)
+ {
+ if (_pollThread != null)
+ {
+ return;
+ }
+
+ Interlocked.Exchange(ref _stopPolling, 0);
+ StartReceiving();
+ Thread worker = new Thread(PollLoop);
+ worker.Start();
+ _pollThread = worker;
+ }
+ }
+
+ ///
+ public void StopPolling()
+ {
+ Thread worker;
+ lock (_pollLock)
+ {
+ Interlocked.Exchange(ref _stopPolling, 1);
+ worker = _pollThread;
+ }
+
+ if (worker == null)
+ {
+ return;
+ }
+
+ if (Thread.CurrentThread != worker)
+ {
+ if (worker.IsAlive)
+ {
+ worker.Join();
+ }
+ }
+
+ lock (_pollLock)
+ {
+ if (Thread.CurrentThread != worker && _pollThread == worker)
+ {
+ _pollThread = null;
+ }
+ }
+ }
+
+ ///
+ /// Gets a value indicating whether DIO1 is high (an IRQ is pending).
+ ///
+ public bool IsDio1High => _dio1Pin.Read() == PinValue.High;
+
+ ///
+ /// Gets the current IRQ status flags from the chip.
+ ///
+ /// The 16-bit IRQ status flags.
+ public ushort GetIrqStatus()
+ {
+ byte[] r = ReadCommand(OpGetIrqStatus, 2);
+ return BinaryPrimitives.ReadUInt16BigEndian(r);
+ }
+
+ ///
+ /// Clears IRQ flags after handling.
+ ///
+ /// Bits to clear in the IRQ status register.
+ public void ClearIrqStatus(ushort mask)
+ {
+ byte[] clearBytes = new byte[2];
+ BinaryPrimitives.WriteUInt16BigEndian(clearBytes, mask);
+ WriteCommand(OpClearIrqStatus, clearBytes);
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ StopPolling();
+
+ if (_resetPin != null)
+ {
+ _resetPin.Dispose();
+ _resetPin = null;
+ }
+
+ if (_busyPin != null)
+ {
+ _busyPin.Dispose();
+ _busyPin = null;
+ }
+
+ if (_dio1Pin != null)
+ {
+ _dio1Pin.Dispose();
+ _dio1Pin = null;
+ }
+
+ if (_disposeSpi && _spi != null)
+ {
+ _spi.Dispose();
+ }
+
+ if (_shouldDispose && _gpio != null)
+ {
+ _gpio.Dispose();
+ }
+
+ _disposed = true;
+ }
+
+ private void PollLoop()
+ {
+ try
+ {
+ while (Interlocked.CompareExchange(ref _stopPolling, 0, 0) == 0)
+ {
+ if (IsDio1High)
+ {
+ try
+ {
+ HandleRxDone();
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine("Sx1262 RX handling error: " + ex.Message);
+ }
+ }
+ else
+ {
+ Thread.Sleep(5);
+ }
+ }
+ }
+ finally
+ {
+ lock (_pollLock)
+ {
+ if (_pollThread == Thread.CurrentThread)
+ {
+ _pollThread = null;
+ }
+ }
+ }
+ }
+
+ internal void WriteCommand(byte opCode, byte[] data)
+ {
+ byte[] tx = new byte[1 + data.Length];
+ tx[0] = opCode;
+ Array.Copy(data, 0, tx, 1, data.Length);
+ WaitBusy(5000);
+ _spi.Write(tx);
+ }
+
+ internal byte[] ReadCommand(byte opCode, int responseLen)
+ {
+ byte[] tx = new byte[2 + responseLen];
+ byte[] rx = new byte[2 + responseLen];
+ tx[0] = opCode;
+ WaitBusy(5000);
+ _spi.TransferFullDuplex(tx, rx);
+ byte[] result = new byte[responseLen];
+ Array.Copy(rx, 2, result, 0, responseLen);
+ return result;
+ }
+ }
+}
diff --git a/devices/LoRa/LoRa/ILoRaDevice.cs b/devices/LoRa/LoRa/ILoRaDevice.cs
new file mode 100644
index 0000000000..41efae0c40
--- /dev/null
+++ b/devices/LoRa/LoRa/ILoRaDevice.cs
@@ -0,0 +1,62 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+
+namespace Iot.Device.LoRa
+{
+ ///
+ /// Abstraction for a LoRa radio transceiver.
+ /// Implement this interface for each chip variant (SX1262, SX1276, RFM95, and similar).
+ ///
+ public interface ILoRaDevice : IDisposable
+ {
+ ///
+ /// Performs a hardware reset and waits for the chip to become ready.
+ /// Must be the first call after construction.
+ ///
+ void Reset();
+
+ ///
+ /// Applies the full initialization sequence. Call once after .
+ ///
+ void Initialize();
+
+ ///
+ /// Sets the RF carrier frequency in Hz (for example 868000000).
+ ///
+ /// Carrier frequency in hertz.
+ void SetRfFrequency(uint frequencyHz);
+
+ ///
+ /// Sends over LoRa, blocking until TxDone or until elapses.
+ /// Callers must supply a non-null, non-empty payload within the implementation's maximum length (for , see ).
+ /// Do not call from (the poll thread); implementations throw if invoked on that thread.
+ ///
+ /// The bytes to transmit.
+ /// Maximum time to wait for completion, in milliseconds. Must be greater than zero.
+ /// Thrown when is null.
+ /// Thrown when is empty.
+ /// Thrown when exceeds the device maximum or is not positive.
+ /// Thrown when TX does not complete in time.
+ /// Thrown when called from the RX poll thread.
+ void Send(byte[] payload, int timeoutMs);
+
+ ///
+ /// Starts a background thread that polls for incoming packets and raises for each valid frame.
+ /// Wire up before calling this method.
+ ///
+ void StartPolling();
+
+ ///
+ /// Signals the poll thread to stop. When called from another thread, blocks until the poll thread exits.
+ /// When called from the poll thread itself (for example from ), returns without blocking; the thread then exits.
+ ///
+ void StopPolling();
+
+ ///
+ /// Raised on the poll thread when a valid packet is received.
+ ///
+ event PacketReceivedHandler PacketReceived;
+ }
+}
diff --git a/devices/LoRa/LoRa/LoRa.nfproj b/devices/LoRa/LoRa/LoRa.nfproj
new file mode 100644
index 0000000000..d9ed110a39
--- /dev/null
+++ b/devices/LoRa/LoRa/LoRa.nfproj
@@ -0,0 +1,83 @@
+
+
+
+
+ $(MSBuildExtensionsPath)\nanoFramework\v1.0\
+
+
+
+ Debug
+ 8.0
+ AnyCPU
+ {11A8DD76-328B-46DF-9F39-F559912D0360};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
+ {5D8A46C7-F705-4AAB-AF5E-D14657161340}
+ Library
+ Properties
+ 512
+ Iot.Device.LoRa
+ Iot.Device.LoRa
+ v1.0
+ false
+ bin\$(Configuration)\Iot.Device.LoRa.xml
+ false
+ $(MSBuildProjectDirectory)\..\Settings.StyleCop
+ true
+ true
+
+
+ true
+
+
+ ..\..\key.snk
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ..\packages\nanoFramework.CoreLibrary.1.17.11\lib\mscorlib.dll
+
+
+ ..\packages\nanoFramework.Runtime.Events.1.11.32\lib\nanoFramework.Runtime.Events.dll
+
+
+ ..\packages\nanoFramework.System.Buffers.Binary.BinaryPrimitives.1.2.862\lib\System.Buffers.Binary.BinaryPrimitives.dll
+
+
+ ..\packages\nanoFramework.System.Device.Gpio.1.1.57\lib\System.Device.Gpio.dll
+
+
+ ..\packages\nanoFramework.System.Device.Spi.1.3.82\lib\System.Device.Spi.dll
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/devices/LoRa/LoRa/LoRaMessage.cs b/devices/LoRa/LoRa/LoRaMessage.cs
new file mode 100644
index 0000000000..87bc6bc2e5
--- /dev/null
+++ b/devices/LoRa/LoRa/LoRaMessage.cs
@@ -0,0 +1,47 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+
+namespace Iot.Device.LoRa
+{
+ ///
+ /// Represents a received LoRa packet.
+ ///
+ public sealed class LoRaMessage
+ {
+ ///
+ /// Gets the raw payload bytes.
+ ///
+ public byte[] Payload { get; }
+
+ ///
+ /// Gets the signal RSSI in dBm (typically -30 to -120).
+ ///
+ public int Rssi { get; }
+
+ ///
+ /// Gets the signal-to-noise ratio in dB.
+ ///
+ public float Snr { get; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The received payload bytes.
+ /// The measured RSSI in dBm.
+ /// The measured SNR in dB.
+ public LoRaMessage(byte[] payload, int rssi, float snr)
+ {
+ if (payload == null)
+ {
+ throw new ArgumentNullException(nameof(payload));
+ }
+
+ Payload = new byte[payload.Length];
+ Array.Copy(payload, Payload, payload.Length);
+ Rssi = rssi;
+ Snr = snr;
+ }
+ }
+}
diff --git a/devices/LoRa/LoRa/PacketReceivedHandler.cs b/devices/LoRa/LoRa/PacketReceivedHandler.cs
new file mode 100644
index 0000000000..5e6e85df14
--- /dev/null
+++ b/devices/LoRa/LoRa/PacketReceivedHandler.cs
@@ -0,0 +1,13 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Iot.Device.LoRa
+{
+ ///
+ /// Represents the method that will handle packet received events.
+ /// .NET nanoFramework does not support generic delegates such as EventHandler<T>, so a custom non-generic delegate is used instead.
+ ///
+ /// The source of the event.
+ /// The received message.
+ public delegate void PacketReceivedHandler(object sender, LoRaMessage message);
+}
diff --git a/devices/LoRa/LoRa/Properties/AssemblyInfo.cs b/devices/LoRa/LoRa/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..1e82136ce4
--- /dev/null
+++ b/devices/LoRa/LoRa/Properties/AssemblyInfo.cs
@@ -0,0 +1,11 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+[assembly: AssemblyTitle("Iot.Device.LoRa")]
+[assembly: AssemblyCompany("nanoFramework Contributors")]
+[assembly: AssemblyCopyright("Copyright (c) .NET Foundation and Contributors")]
+
+[assembly: ComVisible(false)]
diff --git a/devices/LoRa/LoRa/packages.config b/devices/LoRa/LoRa/packages.config
new file mode 100644
index 0000000000..bd1ff5c619
--- /dev/null
+++ b/devices/LoRa/LoRa/packages.config
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/devices/LoRa/LoRa/packages.lock.json b/devices/LoRa/LoRa/packages.lock.json
new file mode 100644
index 0000000000..de6205910a
--- /dev/null
+++ b/devices/LoRa/LoRa/packages.lock.json
@@ -0,0 +1,49 @@
+{
+ "version": 1,
+ "dependencies": {
+ ".NETnanoFramework,Version=v1.0": {
+ "nanoFramework.CoreLibrary": {
+ "type": "Direct",
+ "requested": "[1.17.11, 1.17.11]",
+ "resolved": "1.17.11",
+ "contentHash": "HezzAc0o2XrSGf85xSeD/6xsO6ohF9hX6/iMQ1IZS6Zw6umr4WfAN2Jv0BrPxkaYwzEegJxxZujkHoUIAqtOMw=="
+ },
+ "nanoFramework.Runtime.Events": {
+ "type": "Direct",
+ "requested": "[1.11.32, 1.11.32]",
+ "resolved": "1.11.32",
+ "contentHash": "NyLUIwJDlpl5VKSd+ljmdDtO2WHHBvPvruo1ccaL+hd79z+6XMYze1AccOVXKGiZenLBCwDmFHwpgIQyHkM7GA=="
+ },
+ "nanoFramework.System.Buffers.Binary.BinaryPrimitives": {
+ "type": "Direct",
+ "requested": "[1.2.862, 1.2.862]",
+ "resolved": "1.2.862",
+ "contentHash": "O+jmbbw2h2cjCTShsvWCzjqn/kmRacJE4Ena9gJt5xF5D7XHyYh/uE/qbZ4LhlmlN6JCZjLRNiEfHyM1YUjyxg=="
+ },
+ "nanoFramework.System.Device.Gpio": {
+ "type": "Direct",
+ "requested": "[1.1.57, 1.1.57]",
+ "resolved": "1.1.57",
+ "contentHash": "Es7jHRrT/+0Ty9uJNzJUcTn+aCpjkxXnmsaM+g9HXvLZ8k4SDCCqmO9pT317nX+9ehmgGo2JKrtmkekgbOp+Pw=="
+ },
+ "nanoFramework.System.Device.Spi": {
+ "type": "Direct",
+ "requested": "[1.3.82, 1.3.82]",
+ "resolved": "1.3.82",
+ "contentHash": "kfYc1CNGB12Dd3UO6sgyesf8dLeuycv3z5B6FWEJWCbjDVb24PAUukoDk8oRgr7cBMtmYl1BNja20N5pJ9yYxQ=="
+ },
+ "Nerdbank.GitVersioning": {
+ "type": "Direct",
+ "requested": "[3.9.50, 3.9.50]",
+ "resolved": "3.9.50",
+ "contentHash": "HtOgGF6jZ+WYbXnCUCYPT8Y2d6mIJo9ozjK/FINTRsXdm4Zgv9GehUMa7EFoGQkqrMcDJNOIDwCmENnvXg4UbA=="
+ },
+ "StyleCop.MSBuild": {
+ "type": "Direct",
+ "requested": "[6.2.0, 6.2.0]",
+ "resolved": "6.2.0",
+ "contentHash": "6J51Kt5X+Os+Ckp20SFP1SlLu3tZl+3qBhCMtJUJqGDgwSr4oHT+eg545hXCdp07tRB/8nZfXTOBDdA1XXvjUw=="
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/devices/LoRa/README.md b/devices/LoRa/README.md
new file mode 100644
index 0000000000..1eeddd98c5
--- /dev/null
+++ b/devices/LoRa/README.md
@@ -0,0 +1,106 @@
+# Semtech SX1262 - LoRa transceiver
+
+The [Semtech SX1262](https://www.semtech.com/products/wireless-rf/lora-connect/sx1262) is a sub-GHz LoRa transceiver used on many LoRaWAN and point-to-point modules.
+
+This binding provides **Iot.Device.LoRa** for .NET nanoFramework: **`ILoRaDevice`** for common operations and **`Sx1262`** for SPI radios with reset, BUSY, and DIO1 lines.
+
+## Documentation
+
+- SX1261/2 product page and documentation: [Semtech SX1262](https://www.semtech.com/products/wireless-rf/lora-connect/sx1262#documentation)
+- Repository layout: library in **`LoRa/`**, sample in **`samples/Sx1262Sample/`**, hardware-free tests in **`tests/`**
+
+## Board
+
+LoRa modules that expose SPI (NSS, SCK, MOSI, MISO), **RESET**, **BUSY**, and **DIO1** can be used with **`Sx1262`**. Use **3.3 V** logic and wiring per your module’s pinout.
+
+The sample below follows a **Heltec Vision Master E213 (HT-VME213)** style mapping on **ESP32** with **SPI1**. You can adapt the same pattern to any MCU with an SPI port and three free GPIOs.
+
+> **Tip:** You can add a module photo and a wiring diagram to this folder (for example `sensor.jpg` and a breadboard image) and reference them here, similar to [devices/Nrf24l01](https://github.com/nanoframework/nanoFramework.IoT.Device/tree/main/devices/Nrf24l01).
+
+## Usage
+
+### Hardware required
+
+- SX1262-based module (3.3 V)
+- Host MCU with SPI (the sample targets **ESP32**)
+- Jumper wires
+
+### Connection (sample — ESP32 SPI1, HT-VME213 style)
+
+- VCC — 3.3 V (per module datasheet)
+- GND — GND
+- MOSI — SPI1 MOSI (GPIO **10** in the sample)
+- MISO — SPI1 MISO (GPIO **11**)
+- SCK — SPI1 clock (GPIO **9**)
+- NSS / CS — SPI1 chip select (GPIO **8**)
+- RESET — GPIO output (GPIO **12**)
+- BUSY — GPIO input (GPIO **13**)
+- DIO1 — GPIO input, IRQ (GPIO **14**)
+
+### Code
+
+**Important:** On **ESP32**, configure SPI pin functions before **`SpiDevice.Create`**, and add the **`nanoFramework.Hardware.Esp32`** NuGet package to your application (see **`samples/Sx1262Sample`**).
+
+```csharp
+using System;
+using System.Device.Gpio;
+using System.Device.Spi;
+using System.Diagnostics;
+using System.Text;
+using System.Threading;
+
+using Iot.Device.LoRa;
+using Iot.Device.LoRa.Drivers.Sx1262;
+
+using nanoFramework.Hardware.Esp32;
+
+// Map SPI1 on ESP32 (GPIOs must match your board)
+Configuration.SetPinFunction(10, DeviceFunction.SPI1_MOSI);
+Configuration.SetPinFunction(9, DeviceFunction.SPI1_CLOCK);
+Configuration.SetPinFunction(11, DeviceFunction.SPI1_MISO);
+
+SpiConnectionSettings settings = new SpiConnectionSettings(1, chipSelectLine: 8)
+{
+ ClockFrequency = 1_000_000,
+ Mode = SpiMode.Mode0,
+ DataBitLength = 8
+};
+
+using GpioController gpio = new GpioController();
+using SpiDevice spi = SpiDevice.Create(settings);
+
+// SPI device, reset, BUSY, DIO1; share gpio and do not dispose it from the driver when appropriate
+using (Sx1262 lora = new Sx1262(spi, resetPin: 12, busyPin: 13, dio1Pin: 14, gpioController: gpio, shouldDispose: false))
+{
+ lora.Reset();
+ lora.Initialize();
+
+ lora.PacketReceived += (sender, msg) =>
+ {
+ string text = Encoding.UTF8.GetString(msg.Payload, 0, msg.Payload.Length);
+ Debug.WriteLine("RX: '" + text + "' RSSI=" + msg.Rssi + " dBm SNR=" + msg.Snr + " dB");
+ };
+
+ lora.StartPolling();
+
+ while (true)
+ {
+ string message = "Hello from .NET nanoFramework: " + DateTime.UtcNow;
+ Debug.WriteLine(message);
+ lora.Send(Encoding.UTF8.GetBytes(message), timeoutMs: 3000);
+ Thread.Sleep(10_000);
+ }
+}
+```
+
+For **STM32** and other targets, use your platform’s SPI preset pins and chip select; you do not need **`nanoFramework.Hardware.Esp32`**.
+
+**Usage notes**
+
+- Call **`Reset()`** then **`Initialize()`** once before TX/RX.
+- **`Send`** must not be called from the **`PacketReceived`** callback (poll thread); send from the main loop or another worker thread.
+- Full error handling, cleanup, and **`StopPolling`** are in **`samples/Sx1262Sample/Program.cs`**.
+
+### Result
+
+After deployment, use the debug console to see startup logs, transmitted lines, and received packets with RSSI/SNR. You can capture a screenshot and add **`RunningResult.jpg`** here, as in the [Nrf24l01 sample](https://github.com/nanoframework/nanoFramework.IoT.Device/tree/main/devices/Nrf24l01).
diff --git a/devices/LoRa/Settings.StyleCop b/devices/LoRa/Settings.StyleCop
new file mode 100644
index 0000000000..b28d1f8bfd
--- /dev/null
+++ b/devices/LoRa/Settings.StyleCop
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ True
+
+
+
+
+ True
+
+
+
+
+ True
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ True
+ True
+
+
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+
+
+
diff --git a/devices/LoRa/category.txt b/devices/LoRa/category.txt
new file mode 100644
index 0000000000..d5f4376d41
--- /dev/null
+++ b/devices/LoRa/category.txt
@@ -0,0 +1,2 @@
+wireless
+radio
diff --git a/devices/LoRa/samples/Sx1262Sample/Program.cs b/devices/LoRa/samples/Sx1262Sample/Program.cs
new file mode 100644
index 0000000000..54edd4dc3a
--- /dev/null
+++ b/devices/LoRa/samples/Sx1262Sample/Program.cs
@@ -0,0 +1,162 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Device.Gpio;
+using System.Device.Spi;
+using System.Diagnostics;
+using System.Text;
+using System.Threading;
+
+using Iot.Device.LoRa;
+using Iot.Device.LoRa.Drivers.Sx1262;
+
+using nanoFramework.Hardware.Esp32;
+
+namespace Sx1262Sample
+{
+ ///
+ /// Sample entry point for the SX1262 LoRa transceiver on ESP32 (SPI1).
+ ///
+ public class Program
+ {
+ // ---- SX1262 pin mapping (HT-VME213) - SPI1 ----
+ private const int PinLoraMosi = 10;
+ private const int PinLoraClk = 9;
+ private const int PinLoraMiso = 11;
+ private const int PinLoraCs = 8;
+ private const int PinLoraRst = 12;
+ private const int PinLoraBusy = 13;
+ private const int PinLoraDio1 = 14;
+
+ private static ILoRaDevice _lora;
+
+ ///
+ /// Application entry point: initializes the SX1262, starts RX polling, and sends periodic test frames.
+ ///
+ public static void Main()
+ {
+ GpioController gpio = null;
+ SpiDevice loraSpi = null;
+
+ try
+ {
+ gpio = new GpioController();
+
+ // ---- LoRa ----
+ Configuration.SetPinFunction(PinLoraMosi, DeviceFunction.SPI1_MOSI);
+ Configuration.SetPinFunction(PinLoraClk, DeviceFunction.SPI1_CLOCK);
+ Configuration.SetPinFunction(PinLoraMiso, DeviceFunction.SPI1_MISO);
+
+ SpiConnectionSettings spiSettings = new SpiConnectionSettings(1, PinLoraCs)
+ {
+ ClockFrequency = 1000000,
+ Mode = SpiMode.Mode0,
+ DataBitLength = 8
+ };
+ loraSpi = SpiDevice.Create(spiSettings);
+
+ _lora = new Sx1262(
+ loraSpi,
+ resetPin: PinLoraRst,
+ busyPin: PinLoraBusy,
+ dio1Pin: PinLoraDio1,
+ gpioController: gpio,
+ shouldDispose: false);
+
+ Debug.WriteLine("LoRa SX1262 sample starting...");
+
+ try
+ {
+ _lora.Reset();
+
+ Debug.WriteLine("Initializing LoRa...");
+ _lora.Initialize();
+
+ // PacketReceived is raised from a background thread, so work can continue here without blocking the main thread.
+ _lora.PacketReceived += OnPacketReceived;
+
+ Debug.WriteLine("Starting LoRa receive polling...");
+ _lora.StartPolling();
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine("LoRa initialization failed: " + ex.ToString());
+ CleanupLora();
+ loraSpi?.Dispose();
+ loraSpi = null;
+ gpio?.Dispose();
+ gpio = null;
+ while (true)
+ {
+ Thread.Sleep(60000);
+ }
+ }
+
+ // Send a message every 10 seconds from the main thread only, to avoid concurrency issues with the SX1262 driver.
+ while (true)
+ {
+ DoSend();
+ Thread.Sleep(10000);
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine("Sample startup failed: " + ex.ToString());
+ CleanupLora();
+ loraSpi?.Dispose();
+ gpio?.Dispose();
+ while (true)
+ {
+ Thread.Sleep(60000);
+ }
+ }
+ }
+
+ private static void CleanupLora()
+ {
+ if (_lora == null)
+ {
+ return;
+ }
+
+ try
+ {
+ _lora.PacketReceived -= OnPacketReceived;
+ _lora.StopPolling();
+ _lora.Dispose();
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine("LoRa cleanup failed: " + ex.ToString());
+ }
+
+ _lora = null;
+ }
+
+ // Called from main thread only.
+ private static void DoSend()
+ {
+ try
+ {
+ string message = "Hello from the .NET nanoFramework: " + DateTime.UtcNow;
+ Debug.WriteLine(message);
+ byte[] payload = Encoding.UTF8.GetBytes(message);
+
+ _lora.Send(payload, 3000);
+
+ Debug.WriteLine("Message sent. Whoo Hoo");
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine("TX failed: " + ex.Message);
+ }
+ }
+
+ private static void OnPacketReceived(object sender, LoRaMessage msg)
+ {
+ string text = Encoding.UTF8.GetString(msg.Payload, 0, msg.Payload.Length);
+ Debug.WriteLine("RX: '" + text + "' RSSI=" + msg.Rssi + "dBm SNR=" + msg.Snr + "dB");
+ }
+ }
+}
diff --git a/devices/LoRa/samples/Sx1262Sample/Properties/AssemblyInfo.cs b/devices/LoRa/samples/Sx1262Sample/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..38a28cd5df
--- /dev/null
+++ b/devices/LoRa/samples/Sx1262Sample/Properties/AssemblyInfo.cs
@@ -0,0 +1,11 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+[assembly: AssemblyTitle("Iot.Device.Sx1262.Sample")]
+[assembly: AssemblyCompany("nanoFramework Contributors")]
+[assembly: AssemblyCopyright("Copyright (c) .NET Foundation and Contributors")]
+
+[assembly: ComVisible(false)]
diff --git a/devices/LoRa/samples/Sx1262Sample/Sx1262Sample.nfproj b/devices/LoRa/samples/Sx1262Sample/Sx1262Sample.nfproj
new file mode 100644
index 0000000000..2cf230df5e
--- /dev/null
+++ b/devices/LoRa/samples/Sx1262Sample/Sx1262Sample.nfproj
@@ -0,0 +1,70 @@
+
+
+
+ $(MSBuildExtensionsPath)\nanoFramework\v1.0\
+
+
+
+ Debug
+ 8.0
+ AnyCPU
+ {11A8DD76-328B-46DF-9F39-F559912D0360};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
+ {E4A64F77-59D4-4A2F-B0FE-8315ED264D89}
+ Exe
+ Properties
+ 512
+ Sx1262Sample
+ Sx1262Sample
+ v1.0
+ bin\$(Configuration)\Sx1262Sample.xml
+ 9.0
+ false
+ $(MSBuildProjectDirectory)\..\..\Settings.StyleCop
+
+
+
+
+
+
+
+
+
+
+
+
+ ..\..\packages\nanoFramework.CoreLibrary.1.17.11\lib\mscorlib.dll
+
+
+ ..\..\packages\nanoFramework.Hardware.Esp32.1.6.37\lib\nanoFramework.Hardware.Esp32.dll
+
+
+ ..\..\packages\nanoFramework.Runtime.Events.1.11.32\lib\nanoFramework.Runtime.Events.dll
+
+
+ ..\..\packages\nanoFramework.System.Text.1.3.42\lib\nanoFramework.System.Text.dll
+
+
+ ..\..\packages\nanoFramework.System.Device.Gpio.1.1.57\lib\System.Device.Gpio.dll
+
+
+ ..\..\packages\nanoFramework.System.Device.Spi.1.3.82\lib\System.Device.Spi.dll
+
+
+
+
+
+
+
+
+
+ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
+
+
+
+
+
+
+
+
+
+
diff --git a/devices/LoRa/samples/Sx1262Sample/packages.config b/devices/LoRa/samples/Sx1262Sample/packages.config
new file mode 100644
index 0000000000..200cf33a73
--- /dev/null
+++ b/devices/LoRa/samples/Sx1262Sample/packages.config
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/devices/LoRa/samples/Sx1262Sample/packages.lock.json b/devices/LoRa/samples/Sx1262Sample/packages.lock.json
new file mode 100644
index 0000000000..42af786a1a
--- /dev/null
+++ b/devices/LoRa/samples/Sx1262Sample/packages.lock.json
@@ -0,0 +1,49 @@
+{
+ "version": 1,
+ "dependencies": {
+ ".NETnanoFramework,Version=v1.0": {
+ "nanoFramework.CoreLibrary": {
+ "type": "Direct",
+ "requested": "[1.17.11, 1.17.11]",
+ "resolved": "1.17.11",
+ "contentHash": "HezzAc0o2XrSGf85xSeD/6xsO6ohF9hX6/iMQ1IZS6Zw6umr4WfAN2Jv0BrPxkaYwzEegJxxZujkHoUIAqtOMw=="
+ },
+ "nanoFramework.Hardware.Esp32": {
+ "type": "Direct",
+ "requested": "[1.6.37, 1.6.37]",
+ "resolved": "1.6.37",
+ "contentHash": "vQ9wy4esiBGaxveLPIxwAoBXj3vB0BotHYdTJtFIfgdpkEjsD2synrcD8xTv3kCNoCdptcDO0eSabkqu5eGIAQ=="
+ },
+ "nanoFramework.Runtime.Events": {
+ "type": "Direct",
+ "requested": "[1.11.32, 1.11.32]",
+ "resolved": "1.11.32",
+ "contentHash": "NyLUIwJDlpl5VKSd+ljmdDtO2WHHBvPvruo1ccaL+hd79z+6XMYze1AccOVXKGiZenLBCwDmFHwpgIQyHkM7GA=="
+ },
+ "nanoFramework.System.Text": {
+ "type": "Direct",
+ "requested": "[1.3.42, 1.3.42]",
+ "resolved": "1.3.42",
+ "contentHash": "68HPjhersNpssbmEMUHdMw3073MHfGTfrkbRk9eILKbNPFfPFck7m4y9BlAi6DaguUJaeKxgyIojXF3SQrF8/A=="
+ },
+ "nanoFramework.System.Device.Gpio": {
+ "type": "Direct",
+ "requested": "[1.1.57, 1.1.57]",
+ "resolved": "1.1.57",
+ "contentHash": "Es7jHRrT/+0Ty9uJNzJUcTn+aCpjkxXnmsaM+g9HXvLZ8k4SDCCqmO9pT317nX+9ehmgGo2JKrtmkekgbOp+Pw=="
+ },
+ "nanoFramework.System.Device.Spi": {
+ "type": "Direct",
+ "requested": "[1.3.82, 1.3.82]",
+ "resolved": "1.3.82",
+ "contentHash": "kfYc1CNGB12Dd3UO6sgyesf8dLeuycv3z5B6FWEJWCbjDVb24PAUukoDk8oRgr7cBMtmYl1BNja20N5pJ9yYxQ=="
+ },
+ "StyleCop.MSBuild": {
+ "type": "Direct",
+ "requested": "[6.2.0, 6.2.0]",
+ "resolved": "6.2.0",
+ "contentHash": "6J51Kt5X+Os+Ckp20SFP1SlLu3tZl+3qBhCMtJUJqGDgwSr4oHT+eg545hXCdp07tRB/8nZfXTOBDdA1XXvjUw=="
+ }
+ }
+ }
+}
diff --git a/devices/LoRa/tests/LoRaMessageTests.cs b/devices/LoRa/tests/LoRaMessageTests.cs
new file mode 100644
index 0000000000..85e215eaa4
--- /dev/null
+++ b/devices/LoRa/tests/LoRaMessageTests.cs
@@ -0,0 +1,66 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+
+using Iot.Device.LoRa;
+
+using nanoFramework.TestFramework;
+
+namespace Iot.Device.LoRa.LoraTests
+{
+ ///
+ /// Regression tests for (null payload, defensive copy, immutability).
+ ///
+ [TestClass]
+ public class LoRaMessageTests
+ {
+ ///
+ /// Verifies a null payload throws (avoids NRE in handlers).
+ ///
+ [TestMethod]
+ public void Constructor_NullPayload_ThrowsArgumentNullException()
+ {
+ Assert.Throws(typeof(ArgumentNullException), () => new LoRaMessage(null, 0, 0f));
+ }
+
+ ///
+ /// Verifies the constructor copies payload bytes so later caller mutations do not change the message.
+ ///
+ [TestMethod]
+ public void Constructor_CopiesPayload_DefensiveCopy()
+ {
+ byte[] source = new byte[] { 0x01, 0x02, 0x03 };
+ LoRaMessage message = new LoRaMessage(source, -90, 1.25f);
+
+ source[0] = 0xFF;
+
+ Assert.Equal((byte)0x01, message.Payload[0]);
+ Assert.Equal((byte)0x02, message.Payload[1]);
+ Assert.Equal((byte)0x03, message.Payload[2]);
+ }
+
+ ///
+ /// Verifies empty payloads are allowed (length zero frame).
+ ///
+ [TestMethod]
+ public void Constructor_EmptyPayload_LengthZero()
+ {
+ LoRaMessage message = new LoRaMessage(new byte[0], -100, 0f);
+
+ Assert.Equal(0, message.Payload.Length);
+ }
+
+ ///
+ /// Verifies RSSI and SNR round-trip unchanged.
+ ///
+ [TestMethod]
+ public void Constructor_PreservesRssiAndSnr()
+ {
+ LoRaMessage message = new LoRaMessage(new byte[] { 0x00 }, -72, 3.5f);
+
+ Assert.Equal(-72, message.Rssi);
+ Assert.True(message.Snr > 3.49f && message.Snr < 3.51f);
+ }
+ }
+}
diff --git a/devices/LoRa/tests/LoRaTests.nfproj b/devices/LoRa/tests/LoRaTests.nfproj
new file mode 100644
index 0000000000..0a1b45cf1d
--- /dev/null
+++ b/devices/LoRa/tests/LoRaTests.nfproj
@@ -0,0 +1,76 @@
+
+
+
+ $(MSBuildExtensionsPath)\nanoFramework\v1.0\
+
+
+
+
+
+
+ Debug
+ 8.0
+ AnyCPU
+ {11A8DD76-328B-46DF-9F39-F559912D0360};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
+ da6342a6-c455-450d-9921-cc76a0032f1d
+ Library
+ Properties
+ 512
+ LoraTests
+ NFUnitTest
+ False
+ true
+ UnitTest
+ v1.0
+ true
+ true
+ false
+ $(MSBuildProjectDirectory)\..\Settings.StyleCop
+
+
+
+ $(MSBuildProjectDirectory)\nano.runsettings
+
+
+
+
+
+
+
+
+
+ ..\packages\nanoFramework.CoreLibrary.1.17.11\lib\mscorlib.dll
+
+
+ ..\packages\nanoFramework.Runtime.Events.1.11.32\lib\nanoFramework.Runtime.Events.dll
+
+
+ ..\packages\nanoFramework.System.Device.Gpio.1.1.57\lib\System.Device.Gpio.dll
+
+
+ ..\packages\nanoFramework.System.Device.Spi.1.3.82\lib\System.Device.Spi.dll
+
+
+ ..\packages\nanoFramework.TestFramework.3.0.77\lib\nanoFramework.TestFramework.dll
+
+
+ ..\packages\nanoFramework.TestFramework.3.0.77\lib\nanoFramework.UnitTestLauncher.exe
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
+
+
+
+
\ No newline at end of file
diff --git a/devices/LoRa/tests/Properties/AssemblyInfo.cs b/devices/LoRa/tests/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..56163c4953
--- /dev/null
+++ b/devices/LoRa/tests/Properties/AssemblyInfo.cs
@@ -0,0 +1,34 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyCopyright("Copyright (c) .NET Foundation and Contributors")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/devices/LoRa/tests/Sx1262ConstructorTests.cs b/devices/LoRa/tests/Sx1262ConstructorTests.cs
new file mode 100644
index 0000000000..381afe0c01
--- /dev/null
+++ b/devices/LoRa/tests/Sx1262ConstructorTests.cs
@@ -0,0 +1,29 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+
+using Iot.Device.LoRa.Drivers.Sx1262;
+
+using nanoFramework.TestFramework;
+
+namespace Iot.Device.LoRa.LoraTests
+{
+ ///
+ /// Guards constructor validation on (no SPI hardware required for null check).
+ ///
+ [TestClass]
+ public class Sx1262ConstructorTests
+ {
+ ///
+ /// Verifies a null throws before GPIO/SPI setup.
+ ///
+ [TestMethod]
+ public void Constructor_NullSpiDevice_ThrowsArgumentNullException()
+ {
+ Assert.Throws(
+ typeof(ArgumentNullException),
+ () => new Sx1262(null, resetPin: 0, busyPin: 1, dio1Pin: 2));
+ }
+ }
+}
diff --git a/devices/LoRa/tests/Sx1262DecodeChipModeTests.cs b/devices/LoRa/tests/Sx1262DecodeChipModeTests.cs
new file mode 100644
index 0000000000..ef394b4232
--- /dev/null
+++ b/devices/LoRa/tests/Sx1262DecodeChipModeTests.cs
@@ -0,0 +1,96 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Iot.Device.LoRa.Drivers.Sx1262;
+
+using nanoFramework.TestFramework;
+
+namespace Iot.Device.LoRa.LoraTests
+{
+ ///
+ /// Unit tests for (no hardware required).
+ ///
+ [TestClass]
+ public class Sx1262DecodeChipModeTests
+ {
+ ///
+ /// Verifies decoding of standby-RC mode bits in the status byte.
+ ///
+ [TestMethod]
+ public void DecodeChipMode_StandbyRc_ReturnsStdbyRc()
+ {
+ byte status = (byte)(0x02 << 4);
+ Assert.Equal("STDBY_RC", Sx1262.DecodeChipMode(status));
+ }
+
+ ///
+ /// Verifies decoding of standby-XOSC mode bits in the status byte.
+ ///
+ [TestMethod]
+ public void DecodeChipMode_StandbyXosc_ReturnsStdbyXosc()
+ {
+ byte status = (byte)(0x03 << 4);
+ Assert.Equal("STDBY_XOSC", Sx1262.DecodeChipMode(status));
+ }
+
+ ///
+ /// Verifies decoding of frequency-synthesizer mode bits in the status byte.
+ ///
+ [TestMethod]
+ public void DecodeChipMode_Fs_ReturnsFs()
+ {
+ byte status = (byte)(0x04 << 4);
+ Assert.Equal("FS", Sx1262.DecodeChipMode(status));
+ }
+
+ ///
+ /// Verifies decoding of RX mode bits in the status byte.
+ ///
+ [TestMethod]
+ public void DecodeChipMode_Rx_ReturnsRx()
+ {
+ byte status = (byte)(0x05 << 4);
+ Assert.Equal("RX", Sx1262.DecodeChipMode(status));
+ }
+
+ ///
+ /// Verifies decoding of TX mode bits in the status byte.
+ ///
+ [TestMethod]
+ public void DecodeChipMode_Tx_ReturnsTx()
+ {
+ byte status = (byte)(0x06 << 4);
+ Assert.Equal("TX", Sx1262.DecodeChipMode(status));
+ }
+
+ ///
+ /// Verifies mode code 0 in bits [6:4] maps to unknown.
+ ///
+ [TestMethod]
+ public void DecodeChipMode_Mode0_ReturnsUnknown()
+ {
+ byte status = (byte)(0x00 << 4);
+ Assert.Equal("UNKNOWN", Sx1262.DecodeChipMode(status));
+ }
+
+ ///
+ /// Verifies reserved mode code 7 in bits [6:4] maps to unknown.
+ ///
+ [TestMethod]
+ public void DecodeChipMode_Mode7_ReturnsUnknown()
+ {
+ byte status = (byte)(0x07 << 4);
+ Assert.Equal("UNKNOWN", Sx1262.DecodeChipMode(status));
+ }
+
+ ///
+ /// Verifies unknown mode bits return the unknown label.
+ ///
+ [TestMethod]
+ public void DecodeChipMode_Unknown_ReturnsUnknown()
+ {
+ byte status = (byte)(0x01 << 4);
+ Assert.Equal("UNKNOWN", Sx1262.DecodeChipMode(status));
+ }
+ }
+}
diff --git a/devices/LoRa/tests/nano.runsettings b/devices/LoRa/tests/nano.runsettings
new file mode 100644
index 0000000000..22ca68937d
--- /dev/null
+++ b/devices/LoRa/tests/nano.runsettings
@@ -0,0 +1,17 @@
+
+
+
+
+ .\TestResults
+ 120000
+ net48
+ x64
+
+
+ None
+ False
+ COM3
+
+
+
+
\ No newline at end of file
diff --git a/devices/LoRa/tests/packages.config b/devices/LoRa/tests/packages.config
new file mode 100644
index 0000000000..1abf6e6335
--- /dev/null
+++ b/devices/LoRa/tests/packages.config
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/devices/LoRa/tests/packages.lock.json b/devices/LoRa/tests/packages.lock.json
new file mode 100644
index 0000000000..541c8f1ac8
--- /dev/null
+++ b/devices/LoRa/tests/packages.lock.json
@@ -0,0 +1,43 @@
+{
+ "version": 1,
+ "dependencies": {
+ ".NETnanoFramework,Version=v1.0": {
+ "nanoFramework.CoreLibrary": {
+ "type": "Direct",
+ "requested": "[1.17.11, 1.17.11]",
+ "resolved": "1.17.11",
+ "contentHash": "HezzAc0o2XrSGf85xSeD/6xsO6ohF9hX6/iMQ1IZS6Zw6umr4WfAN2Jv0BrPxkaYwzEegJxxZujkHoUIAqtOMw=="
+ },
+ "nanoFramework.Runtime.Events": {
+ "type": "Direct",
+ "requested": "[1.11.32, 1.11.32]",
+ "resolved": "1.11.32",
+ "contentHash": "NyLUIwJDlpl5VKSd+ljmdDtO2WHHBvPvruo1ccaL+hd79z+6XMYze1AccOVXKGiZenLBCwDmFHwpgIQyHkM7GA=="
+ },
+ "nanoFramework.System.Device.Gpio": {
+ "type": "Direct",
+ "requested": "[1.1.57, 1.1.57]",
+ "resolved": "1.1.57",
+ "contentHash": "Es7jHRrT/+0Ty9uJNzJUcTn+aCpjkxXnmsaM+g9HXvLZ8k4SDCCqmO9pT317nX+9ehmgGo2JKrtmkekgbOp+Pw=="
+ },
+ "nanoFramework.System.Device.Spi": {
+ "type": "Direct",
+ "requested": "[1.3.82, 1.3.82]",
+ "resolved": "1.3.82",
+ "contentHash": "kfYc1CNGB12Dd3UO6sgyesf8dLeuycv3z5B6FWEJWCbjDVb24PAUukoDk8oRgr7cBMtmYl1BNja20N5pJ9yYxQ=="
+ },
+ "nanoFramework.TestFramework": {
+ "type": "Direct",
+ "requested": "[3.0.77, 3.0.77]",
+ "resolved": "3.0.77",
+ "contentHash": "Py5W1oN84KMBmOOHCzdz6pyi3bZTnQu9BoqIx0KGqkhG3V8kGoem/t+BuCM0pMIWAyl2iMP1n2S9624YXmBJZw=="
+ },
+ "StyleCop.MSBuild": {
+ "type": "Direct",
+ "requested": "[6.2.0, 6.2.0]",
+ "resolved": "6.2.0",
+ "contentHash": "6J51Kt5X+Os+Ckp20SFP1SlLu3tZl+3qBhCMtJUJqGDgwSr4oHT+eg545hXCdp07tRB/8nZfXTOBDdA1XXvjUw=="
+ }
+ }
+ }
+}
diff --git a/devices/LoRa/version.json b/devices/LoRa/version.json
new file mode 100644
index 0000000000..34e51c8b9f
--- /dev/null
+++ b/devices/LoRa/version.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
+ "version": "1.0",
+ "semVer1NumericIdentifierPadding": 3,
+ "nuGetPackageVersion": {
+ "semVer": 2.0
+ },
+ "publicReleaseRefSpec": [
+ "^refs/heads/develop$",
+ "^refs/heads/main$",
+ "^refs/heads/v\\d+(?:\\.\\d+)?$"
+ ],
+ "cloudBuild": {
+ "setAllVariables": true
+ }
+}