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 + } +}