mirror of
https://github.com/tinygo-org/drivers.git
synced 2026-07-27 11:08:41 +00:00
Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 820298bfe3 | |||
| 46cbacbe03 | |||
| 9d4847fc6d | |||
| 4de0afc224 | |||
| 21fa184b76 | |||
| fae4307190 | |||
| 7f61bda24c | |||
| e613ab1eb3 | |||
| d57d9b6c74 | |||
| 07ba9ec0fd | |||
| fa0cf6ec7f | |||
| 4e687b29c3 | |||
| d283abdb3a | |||
| 02d808a6fb | |||
| d473b9e1dc | |||
| 7cc32dffed | |||
| 58cb44f3f6 | |||
| 78cc1afe46 | |||
| e6cde8f7ae | |||
| 89f113c0ca | |||
| 3b8a808a52 | |||
| 7506a895a2 | |||
| 4a950a4474 | |||
| f2637a87d2 | |||
| ecb343b3b3 | |||
| 0b62d4fb4a | |||
| 5b461ec2d1 | |||
| b1c9612158 | |||
| 0b6e1d0e78 | |||
| 49e7a6d6c8 | |||
| be644b36fe | |||
| 409723e496 | |||
| 8aa3530c8b | |||
| 390854ee45 | |||
| ce5dd65dad | |||
| 73e9d5bfec | |||
| 462d2b3315 | |||
| 9e70ecbe64 | |||
| d6d06396b4 | |||
| 80b2a4a569 | |||
| 556aa9e6d3 | |||
| 4bc8ecdbdd | |||
| 999f42dc03 | |||
| a1b47cd778 | |||
| 889536f7f0 | |||
| 301e6bc35b | |||
| 49b390f7bc | |||
| a62fc01d81 | |||
| 36dfdf568e | |||
| 7b5a3970ca | |||
| 3433364586 | |||
| 338be928c9 | |||
| 98943393a5 | |||
| 6b817faed3 | |||
| 9b14ee3ec5 | |||
| 6d51370512 | |||
| 2005d7d483 | |||
| f15eec822f | |||
| dc4f0c974a | |||
| 28ddb2681d |
@@ -13,8 +13,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
container: tinygo/tinygo-dev
|
||||
steps:
|
||||
- name: Work around CVE-2022-24765
|
||||
# We're not on a multi-user machine, so this is safe.
|
||||
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: TinyGo version check
|
||||
run: tinygo version
|
||||
- name: Enforce Go Formatted Code
|
||||
|
||||
@@ -1,3 +1,80 @@
|
||||
0.24.0
|
||||
---
|
||||
- **new devices**
|
||||
- **lora**
|
||||
- created shared RadioEvent
|
||||
- move shared config for sx126x/sx127x to single package
|
||||
- **lorawan**
|
||||
- add initial LoRaWAN stack support
|
||||
- Basic implementation of Lorawan Regional Settings and EU868/AU915 regions
|
||||
- **qmi8658c**
|
||||
- Add support for the QMI8658C sensor (#467)
|
||||
- **sh1106**
|
||||
- add support for SH1106 display driver
|
||||
- **sx127x**
|
||||
- Driver for Semtech sx127x radio modules
|
||||
|
||||
- **enhancements**
|
||||
- **bme280**
|
||||
- improve config support
|
||||
- add ReadAltitude() function copied from BMP280 driver
|
||||
- **buzzer**
|
||||
- make all note durations float64
|
||||
- no tone during rest
|
||||
- **dht22**
|
||||
- update DHT22 receive to use runtime/interrupt
|
||||
- **gps**
|
||||
- add support for GLL sentence type, add original sentence to gps errors
|
||||
- improve error handling
|
||||
- improve parsing and add tests to verify
|
||||
- **microbitmatrix**
|
||||
- add link to schema for microbit V2
|
||||
- add smoke test for microbitmatrix with microbit-v2
|
||||
- add support for brightness of led pixels
|
||||
- harmonize v1 and v2 implementation
|
||||
- move Size() to version agnostic part
|
||||
- **mpu6050**
|
||||
- add functions to configure clock, and scaling for accelerometer and gyroscope
|
||||
- **net/http**
|
||||
- add PostForm()
|
||||
- **sx126x**
|
||||
- add Reset() and needed pin
|
||||
- move RadioController into separate file for clarity
|
||||
- pre-define all errors to avoid heap allocations
|
||||
- refactor to RadioController interface to more easily handle non-STM32WL boards and remove duplicated code
|
||||
|
||||
- **vl53l1x**
|
||||
- Add getter for the effective SPAD count
|
||||
- **wifinina**
|
||||
- add support for http server (#480)
|
||||
|
||||
- **bugfixes**
|
||||
- **lsm303agr**
|
||||
- fix I2C address auto increment for multi data read
|
||||
- **net**
|
||||
- (#501) make IP.String() method return something sensible
|
||||
- **mpu6050**
|
||||
- return I2C error when configuring fails
|
||||
- **sx126x**
|
||||
- fix in SetBandwidth function
|
||||
- actually set the frequency when calling SetFrequency()
|
||||
- correct RX/TX pin mapping for TheThingsIndustries GNSE board
|
||||
|
||||
- **examples**
|
||||
- **LoRaWAN**
|
||||
- example with LoRaWAN AT command set implementation
|
||||
- basic example
|
||||
- update all remaining examples for refactored API
|
||||
- **sx126x**
|
||||
- fix bandwidth,tx power in lora//lora_continuous example
|
||||
- **sx127x**
|
||||
- rx/tx example
|
||||
|
||||
- **build**
|
||||
- remove older format build tags
|
||||
- update to actions/checkout@v3
|
||||
- work around for CVE-2022-24765
|
||||
|
||||
0.23.0
|
||||
---
|
||||
- **new devices**
|
||||
|
||||
@@ -249,6 +249,9 @@ endif
|
||||
@md5sum ./build/test.uf2
|
||||
tinygo build -size short -o ./build/test.uf2 -target=circuitplay-express ./examples/makeybutton/main.go
|
||||
@md5sum ./build/test.uf2
|
||||
tinygo build -size short -o ./build/test.hex -target=nucleo-wl55jc ./examples/lora/lorawan/atcmd/
|
||||
@md5sum ./build/test.hex
|
||||
|
||||
|
||||
# rwildcard is a recursive version of $(wildcard)
|
||||
# https://blog.jgc.org/2011/07/gnu-make-recursive-wildcard-function.html
|
||||
|
||||
@@ -146,6 +146,7 @@ The following 90 devices are supported.
|
||||
| [WS2812 RGB LED](https://cdn-shop.adafruit.com/datasheets/WS2812.pdf) | GPIO |
|
||||
| [XPT2046 touch controller](http://grobotronics.com/images/datasheets/xpt2046-datasheet.pdf) | GPIO |
|
||||
| [Semtech SX126x Lora](https://www.semtech.com/products/wireless-rf/lora-connect/sx1261) | SPI |
|
||||
| [Semtech SX127x Lora](https://www.semtech.com/products/wireless-rf/lora-connect/sx1276) | SPI |
|
||||
| [SSD1289 TFT color display](http://aitendo3.sakura.ne.jp/aitendo_data/product_img/lcd/tft2/M032C1289TP/3.2-SSD1289.pdf) | GPIO |
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
package bmp180 // import "tinygo.org/x/drivers/bmp180"
|
||||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
@@ -123,6 +124,21 @@ func (d *Device) ReadPressure() (pressure int32, err error) {
|
||||
return 1000 * (p + ((x1 + x2 + 3791) >> 4)), nil
|
||||
}
|
||||
|
||||
// ReadAltitude returns the current altitude in meters based on the
|
||||
// current barometric pressure and estimated pressure at sea level.
|
||||
// Calculation is based on code from Adafruit BME280 library
|
||||
//
|
||||
// https://github.com/adafruit/Adafruit_BME280_Library
|
||||
func (d *Device) ReadAltitude() (int32, error) {
|
||||
mPa, err := d.ReadPressure()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
atmP := float32(mPa) / 100000
|
||||
|
||||
return int32(44330.0 * (1.0 - math.Pow(float64(atmP/SEALEVEL_PRESSURE), 0.1903))), nil
|
||||
}
|
||||
|
||||
// rawTemp returns the sensor's raw values of the temperature
|
||||
func (d *Device) rawTemp() (int32, error) {
|
||||
d.bus.WriteRegister(uint8(d.Address), REG_CTRL, []byte{CMD_TEMP})
|
||||
|
||||
@@ -28,3 +28,7 @@ const (
|
||||
// ULTRAHIGHRESOLUTION is the highest oversampling mode of the pressure measurement.
|
||||
ULTRAHIGHRESOLUTION
|
||||
)
|
||||
|
||||
const (
|
||||
SEALEVEL_PRESSURE float32 = 1013.25 // in hPa
|
||||
)
|
||||
|
||||
@@ -27,6 +27,9 @@ func main() {
|
||||
pressure, _ := sensor.ReadPressure()
|
||||
println("Pressure", float32(pressure)/100000, "hPa")
|
||||
|
||||
altitude, _ := sensor.ReadAltitude()
|
||||
println("Altitude", altitude, "meters")
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
# AT-CMD implementation of at-lora command set
|
||||
|
||||
```
|
||||
$ tinygo monitor
|
||||
Connected to /dev/ttyACM0. Press Ctrl-C to exit.
|
||||
+AT: OK
|
||||
+VER: 0.0.1 (sx127x v18)
|
||||
```
|
||||
|
||||
# Building
|
||||
|
||||
## Simulator
|
||||
|
||||
```
|
||||
tinygo flash -target pico ./examples/lora/atcmd/
|
||||
```
|
||||
|
||||
## PyBadge with LoRa Featherwing
|
||||
|
||||
```
|
||||
tinygo flash -target pybadge -tags featherwing ./examples/lora/atcmd/
|
||||
```
|
||||
|
||||
## LoRa-E5
|
||||
|
||||
```
|
||||
tinygo flash -target lorae5 ./examples/lora/atcmd/
|
||||
```
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
//go:build gnse || lorae5 || nucleowl55jc
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"device/stm32"
|
||||
"machine"
|
||||
"runtime/interrupt"
|
||||
|
||||
rfswitch "tinygo.org/x/drivers/examples/sx126x/rfswitch"
|
||||
|
||||
"tinygo.org/x/drivers/lora"
|
||||
"tinygo.org/x/drivers/sx126x"
|
||||
)
|
||||
|
||||
const (
|
||||
FREQ = 868100000
|
||||
LORA_DEFAULT_RXTIMEOUT_MS = 1000
|
||||
LORA_DEFAULT_TXTIMEOUT_MS = 5000
|
||||
)
|
||||
|
||||
var (
|
||||
loraRadio *sx126x.Device
|
||||
)
|
||||
|
||||
// do sx126x setup here
|
||||
func setupLora() (LoraRadio, error) {
|
||||
loraRadio = sx126x.New(machine.SPI3)
|
||||
loraRadio.SetDeviceType(sx126x.DEVICE_TYPE_SX1262)
|
||||
|
||||
// Create RF Switch
|
||||
var radioSwitch rfswitch.CustomSwitch
|
||||
loraRadio.SetRfSwitch(radioSwitch)
|
||||
|
||||
if state := loraRadio.DetectDevice(); !state {
|
||||
return nil, errRadioNotFound
|
||||
}
|
||||
|
||||
// Add interrupt handler for Radio IRQs
|
||||
intr := interrupt.New(stm32.IRQ_Radio_IRQ_Busy, radioIntHandler)
|
||||
intr.Enable()
|
||||
|
||||
loraConf := lora.Config{
|
||||
Freq: FREQ,
|
||||
Bw: lora.Bandwidth_500_0,
|
||||
Sf: lora.SpreadingFactor9,
|
||||
Cr: lora.CodingRate4_7,
|
||||
HeaderType: lora.HeaderExplicit,
|
||||
Preamble: 12,
|
||||
Ldr: lora.LowDataRateOptimizeOff,
|
||||
Iq: lora.IQStandard,
|
||||
Crc: lora.CRCOn,
|
||||
SyncWord: lora.SyncPrivate,
|
||||
LoraTxPowerDBm: 20,
|
||||
}
|
||||
|
||||
loraRadio.LoraConfig(loraConf)
|
||||
|
||||
return loraRadio, nil
|
||||
}
|
||||
|
||||
// radioIntHandler will take care of radio interrupts
|
||||
func radioIntHandler(intr interrupt.Interrupt) {
|
||||
loraRadio.HandleInterrupt()
|
||||
}
|
||||
|
||||
func firmwareVersion() string {
|
||||
return "sx126x"
|
||||
}
|
||||
|
||||
func lorarx() ([]byte, error) {
|
||||
return loraRadio.Rx(LORA_DEFAULT_RXTIMEOUT_MS)
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
//go:build featherwing
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"machine"
|
||||
|
||||
"tinygo.org/x/drivers/lora"
|
||||
"tinygo.org/x/drivers/sx127x"
|
||||
)
|
||||
|
||||
const (
|
||||
FREQ = 868100000
|
||||
LORA_DEFAULT_RXTIMEOUT_MS = 1000
|
||||
LORA_DEFAULT_TXTIMEOUT_MS = 5000
|
||||
)
|
||||
|
||||
var (
|
||||
// We assume LoRa Featherwing module is connected to PyBadge:
|
||||
rstPin = machine.D11
|
||||
csPin = machine.D10
|
||||
dio0Pin = machine.D6
|
||||
dio1Pin = machine.D9
|
||||
spi = machine.SPI0
|
||||
loraRadio *sx127x.Device
|
||||
)
|
||||
|
||||
// do sx127x setup here
|
||||
func setupLora() (LoraRadio, error) {
|
||||
rstPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
csPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
dio0Pin.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
|
||||
dio1Pin.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
|
||||
spi.Configure(machine.SPIConfig{Frequency: 500000, Mode: 0})
|
||||
|
||||
loraRadio = sx127x.New(spi, csPin, rstPin)
|
||||
loraRadio.Reset()
|
||||
|
||||
if state := loraRadio.DetectDevice(); !state {
|
||||
return nil, errRadioNotFound
|
||||
}
|
||||
|
||||
// Setup DIO0 interrupt Handling
|
||||
if err := dio0Pin.SetInterrupt(machine.PinRising, dioIrqHandler); err != nil {
|
||||
println("could not configure DIO0 pin interrupt:", err.Error())
|
||||
}
|
||||
|
||||
// Setup DIO1 interrupt Handling
|
||||
if err := dio1Pin.SetInterrupt(machine.PinRising, dioIrqHandler); err != nil {
|
||||
println("could not configure DIO1 pin interrupt:", err.Error())
|
||||
}
|
||||
|
||||
// Prepare for Lora Operation
|
||||
loraConf := lora.Config{
|
||||
Freq: FREQ,
|
||||
Bw: lora.Bandwidth_500_0,
|
||||
Sf: lora.SpreadingFactor9,
|
||||
Cr: lora.CodingRate4_7,
|
||||
HeaderType: lora.HeaderExplicit,
|
||||
Preamble: 12,
|
||||
Iq: lora.IQStandard,
|
||||
Crc: lora.CRCOn,
|
||||
SyncWord: lora.SyncPrivate,
|
||||
LoraTxPowerDBm: 20,
|
||||
}
|
||||
|
||||
loraRadio.LoraConfig(loraConf)
|
||||
|
||||
return loraRadio, nil
|
||||
}
|
||||
|
||||
func dioIrqHandler(machine.Pin) {
|
||||
loraRadio.HandleInterrupt()
|
||||
}
|
||||
|
||||
func firmwareVersion() string {
|
||||
v := loraRadio.GetVersion()
|
||||
return "sx127x v" + strconv.Itoa(int(v))
|
||||
}
|
||||
|
||||
func lorarx() ([]byte, error) {
|
||||
return loraRadio.Rx(LORA_DEFAULT_RXTIMEOUT_MS)
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package main
|
||||
|
||||
const VERSION = "0.0.1"
|
||||
|
||||
func currentVersion() string {
|
||||
return VERSION
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
# AT-CMD implementation of at-lora command set
|
||||
|
||||
This example implements the AT command set as used by Seeed in the LoRa-E5 series of boards, but in the form of a TinyGo program that provides a serial interface.
|
||||
|
||||
See https://files.seeedstudio.com/products/317990687/res/LoRa-E5%20AT%20Command%20Specification_V1.0%20.pdf for more information.
|
||||
|
||||
```
|
||||
$ tinygo monitor
|
||||
Connected to /dev/ttyACM0. Press Ctrl-C to exit.
|
||||
+AT: OK
|
||||
+VER: 0.0.1 (sx127x v18)
|
||||
```
|
||||
|
||||
# Building
|
||||
|
||||
Run the following commands from the main `drivers` directory.
|
||||
|
||||
## Simulator
|
||||
|
||||
Builds/flashes atcmd console application with simulator instead of actual LoRa radio.
|
||||
|
||||
```
|
||||
tinygo flash -target pico ./examples/lora/lorawan/atcmd/
|
||||
```
|
||||
|
||||
## PyBadge with LoRa Featherwing
|
||||
|
||||
Builds/flashes atcmd console application on PyBadge using LoRa Featherwing (RFM95/SX1276).
|
||||
|
||||
```
|
||||
tinygo flash -target pybadge -tags featherwing ./examples/lora/lorawan/atcmd/
|
||||
```
|
||||
|
||||
## LoRa-E5
|
||||
|
||||
Builds/flashes atcmd console application on Lora-E5 using onboard SX126x.
|
||||
|
||||
```
|
||||
tinygo flash -target lorae5 ./examples/lora/lorawan/atcmd/
|
||||
```
|
||||
|
||||
## Joining a Public Lorawan Network
|
||||
|
||||
```
|
||||
AT+ID=DevEui,0101010101010101
|
||||
AT+ID=AppEui,0123012301230213
|
||||
AT+KEY=APPKEY,AEAEAEAEAEAEAEAAEAEAEAEAEAEAAEAE
|
||||
AT+LW=NET,ON
|
||||
AT+JOIN
|
||||
```
|
||||
|
||||
AT+LW=NET,(ON|OFF) command changes Lora Sync Word to connect on public network(ON) or private networks(OFF)
|
||||
@@ -3,6 +3,9 @@ package main
|
||||
import (
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
|
||||
"tinygo.org/x/drivers/examples/lora/lorawan/common"
|
||||
"tinygo.org/x/drivers/lora/lorawan"
|
||||
)
|
||||
|
||||
// Use to test if connection to module is OK.
|
||||
@@ -12,7 +15,7 @@ func quicktest() {
|
||||
|
||||
// Check firmware version.
|
||||
func version() {
|
||||
writeCommandOutput("VER", currentVersion()+" ("+firmwareVersion()+")")
|
||||
writeCommandOutput("VER", common.CurrentVersion()+" ("+common.FirmwareVersion()+")")
|
||||
}
|
||||
|
||||
// Use to check the ID of the LoRaWAN module, or change the ID.
|
||||
@@ -23,13 +26,42 @@ func id(args string) error {
|
||||
param, val, hasComma := strings.Cut(args, ",")
|
||||
if hasComma {
|
||||
// set
|
||||
data := strings.Trim(val, "\"'")
|
||||
|
||||
// convert data from hex formatted string
|
||||
data = strings.ReplaceAll(data, " ", "")
|
||||
hexdata, err := hex.DecodeString(data)
|
||||
if err != nil {
|
||||
writeCommandOutput(cmd, err.Error())
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
switch param {
|
||||
case "DevAddr":
|
||||
writeCommandOutput(cmd, "DevAddr, "+val)
|
||||
if err := session.SetDevAddr(hexdata); err != nil {
|
||||
writeCommandOutput(cmd, err.Error())
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
writeCommandOutput(cmd, "DevAddr, "+session.GetDevAddr())
|
||||
case "DevEui":
|
||||
writeCommandOutput(cmd, "DevEui, "+val)
|
||||
if err := otaa.SetDevEUI(hexdata); err != nil {
|
||||
writeCommandOutput(cmd, err.Error())
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
writeCommandOutput(cmd, "DevEui, "+otaa.GetDevEUI())
|
||||
case "AppEui":
|
||||
writeCommandOutput(cmd, "AppEui, "+val)
|
||||
if err := otaa.SetAppEUI(hexdata); err != nil {
|
||||
writeCommandOutput(cmd, err.Error())
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
writeCommandOutput(cmd, "AppEui, "+otaa.GetAppEUI())
|
||||
default:
|
||||
return errInvalidCommand
|
||||
}
|
||||
@@ -40,15 +72,15 @@ func id(args string) error {
|
||||
// get
|
||||
switch param {
|
||||
case "DevAddr":
|
||||
writeCommandOutput(cmd, "DevAddr, xx:xx:xx:xx")
|
||||
writeCommandOutput(cmd, "DevAddr, "+session.GetDevAddr())
|
||||
case "DevEui":
|
||||
writeCommandOutput(cmd, "DevEui, xx:xx:xx:xx:xx:xx:xx:xx")
|
||||
writeCommandOutput(cmd, "DevEui, "+otaa.GetDevEUI())
|
||||
case "AppEui":
|
||||
writeCommandOutput(cmd, "AppEui, xx:xx:xx:xx:xx:xx:xx:xx")
|
||||
writeCommandOutput(cmd, "AppEui, "+otaa.GetAppEUI())
|
||||
default:
|
||||
writeCommandOutput(cmd, "DevAddr, xx:xx:xx:xx")
|
||||
writeCommandOutput(cmd, "DevEui, xx:xx:xx:xx:xx:xx:xx:xx")
|
||||
writeCommandOutput(cmd, "AppEui, xx:xx:xx:xx:xx:xx:xx:xx")
|
||||
writeCommandOutput(cmd, "DevAddr, "+session.GetDevAddr())
|
||||
writeCommandOutput(cmd, "DevEui, "+otaa.GetDevEUI())
|
||||
writeCommandOutput(cmd, "AppEui, "+otaa.GetAppEUI())
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -208,9 +240,38 @@ func rxwin1(setting string) error {
|
||||
|
||||
func key(setting string) error {
|
||||
cmd := "KEY"
|
||||
writeCommandOutput(cmd, setting)
|
||||
|
||||
return nil
|
||||
// look for comma in args
|
||||
param, val, hasComma := strings.Cut(setting, ",")
|
||||
if hasComma {
|
||||
// set
|
||||
data := strings.Trim(val, "\"'")
|
||||
|
||||
// convert data from hex formatted string
|
||||
data = strings.ReplaceAll(data, " ", "")
|
||||
hexdata, err := hex.DecodeString(data)
|
||||
if err != nil {
|
||||
writeCommandOutput(cmd, err.Error())
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
switch param {
|
||||
case "APPKEY":
|
||||
if err := otaa.SetAppKey(hexdata); err != nil {
|
||||
writeCommandOutput(cmd, err.Error())
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
writeCommandOutput(cmd, "APPKEY, "+otaa.GetAppKey())
|
||||
default:
|
||||
return errInvalidCommand
|
||||
}
|
||||
}
|
||||
|
||||
// cannot get keys
|
||||
return errInvalidCommand
|
||||
}
|
||||
|
||||
func fdefault(setting string) error {
|
||||
@@ -228,9 +289,23 @@ func mode(setting string) error {
|
||||
}
|
||||
|
||||
func join(setting string) error {
|
||||
// TODO: check that DevEUI, AppEUI, and AppKey have values
|
||||
|
||||
cmd := "JOIN"
|
||||
writeCommandOutput(cmd, "Starting")
|
||||
if err := lorawan.Join(otaa, session); err != nil {
|
||||
writeCommandOutput(cmd, err.Error())
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
writeCommandOutput(cmd, "Network joined")
|
||||
writeCommandOutput(cmd, "DevEui, "+otaa.GetDevEUI())
|
||||
writeCommandOutput(cmd, "AppEui, "+otaa.GetAppEUI())
|
||||
writeCommandOutput(cmd, "DevAddr, "+session.GetDevAddr())
|
||||
writeCommandOutput(cmd, "NetID, "+otaa.GetNetID())
|
||||
writeCommandOutput(cmd, "NwkSKey, "+session.GetNwkSKey())
|
||||
writeCommandOutput(cmd, "AppSKey, "+session.GetAppSKey())
|
||||
writeCommandOutput(cmd, "Done")
|
||||
return nil
|
||||
}
|
||||
@@ -262,6 +337,17 @@ func delay(setting string) error {
|
||||
|
||||
func lw(setting string) error {
|
||||
cmd := "LW"
|
||||
|
||||
param, val, hasComma := strings.Cut(setting, ",")
|
||||
if hasComma {
|
||||
if param == "NET" {
|
||||
if val == "ON" {
|
||||
lorawan.SetPublicNetwork(true)
|
||||
} else {
|
||||
lorawan.SetPublicNetwork(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
writeCommandOutput(cmd, setting)
|
||||
|
||||
return nil
|
||||
@@ -376,7 +462,7 @@ func sendhex(data string) error {
|
||||
func recv(setting string) error {
|
||||
cmd := "RECV"
|
||||
|
||||
data, err := lorarx()
|
||||
data, err := common.Lorarx()
|
||||
if err != nil {
|
||||
writeCommandOutput(cmd, "ERROR "+err.Error())
|
||||
return err
|
||||
@@ -389,7 +475,7 @@ func recv(setting string) error {
|
||||
func recvhex(setting string) error {
|
||||
cmd := "RECVHEX"
|
||||
|
||||
data, err := lorarx()
|
||||
data, err := common.Lorarx()
|
||||
if err != nil {
|
||||
writeCommandOutput(cmd, "ERROR "+err.Error())
|
||||
return err
|
||||
@@ -11,6 +11,11 @@ package main
|
||||
import (
|
||||
"machine"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers/examples/lora/lorawan/common"
|
||||
"tinygo.org/x/drivers/lora"
|
||||
"tinygo.org/x/drivers/lora/lorawan"
|
||||
"tinygo.org/x/drivers/lora/lorawan/region"
|
||||
)
|
||||
|
||||
// change these to test a different UART or pins if available
|
||||
@@ -20,7 +25,10 @@ var (
|
||||
rx = machine.UART_RX_PIN
|
||||
input = make([]byte, 0, 64)
|
||||
|
||||
radio LoraRadio
|
||||
radio lora.Radio
|
||||
session *lorawan.Session
|
||||
otaa *lorawan.Otaa
|
||||
|
||||
defaultTimeout uint32 = 1000
|
||||
)
|
||||
|
||||
@@ -28,11 +36,17 @@ func main() {
|
||||
uart.Configure(machine.UARTConfig{TX: tx, RX: rx})
|
||||
|
||||
var err error
|
||||
radio, err = setupLora()
|
||||
radio, err = common.SetupLora()
|
||||
if err != nil {
|
||||
fail(err.Error())
|
||||
}
|
||||
|
||||
session = &lorawan.Session{}
|
||||
otaa = &lorawan.Otaa{}
|
||||
lorawan.UseRadio(radio)
|
||||
|
||||
lorawan.UseRegionSettings(region.EU868())
|
||||
|
||||
for {
|
||||
if uart.Buffered() > 0 {
|
||||
data, _ := uart.ReadByte()
|
||||
@@ -0,0 +1,44 @@
|
||||
# Simple Lorawan example
|
||||
|
||||
This demo code will connect Lorawan network and send sample uplink message
|
||||
|
||||
You may change your Lorawan keys (AppEUI, DevEUI, AppKEY) in key-default.go
|
||||
|
||||
|
||||
```
|
||||
$ tinygo monitor
|
||||
Connected to /dev/ttyACM0. Press Ctrl-C to exit.
|
||||
Lorawan Simple Demo
|
||||
Start Lorawan Join sequence
|
||||
loraConnect: Connected !
|
||||
```
|
||||
|
||||
# Building
|
||||
|
||||
## Simulator
|
||||
|
||||
```
|
||||
tinygo flash -target pico ./examples/lora/lorawan/basic-demo
|
||||
```
|
||||
|
||||
## PyBadge with LoRa Featherwing
|
||||
|
||||
```
|
||||
tinygo flash -target pybadge -tags featherwing ./examples/lora/lorawan/basic-demo
|
||||
```
|
||||
|
||||
## LoRa-E5
|
||||
|
||||
```
|
||||
tinygo flash -target lorae5 ./examples/lora/lorawan/basic-demo
|
||||
```
|
||||
|
||||
|
||||
## Enable debugging
|
||||
|
||||
You can also enable some debug logs with ldflags :
|
||||
|
||||
```
|
||||
$ tinygo build -ldflags="-X 'main.debug=true'" -target=lorae5
|
||||
```
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
//go:build !customkeys
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"tinygo.org/x/drivers/lora/lorawan"
|
||||
)
|
||||
|
||||
// These are sample keys, so the example builds
|
||||
// Either change here, or create a new go file and use customkeys build tag
|
||||
func setLorawanKeys() {
|
||||
otaa.SetAppEUI([]uint8{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})
|
||||
otaa.SetDevEUI([]uint8{0xB3, 0xD5, 0x41, 0x00, 0x0A, 0xF1, 0xA4, 0x45})
|
||||
otaa.SetAppKey([]uint8{0x12, 0x22, 0xA3, 0xFF, 0x0C, 0x7B, 0x76, 0x7B, 0x8F, 0xD3, 0x12, 0x4F, 0xCE, 0x7A, 0x32, 0x16})
|
||||
lorawan.SetPublicNetwork(true)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// Simple code for connecting to Lorawan network and uploading sample payload
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers/examples/lora/lorawan/common"
|
||||
"tinygo.org/x/drivers/lora"
|
||||
"tinygo.org/x/drivers/lora/lorawan"
|
||||
"tinygo.org/x/drivers/lora/lorawan/region"
|
||||
)
|
||||
|
||||
var debug string
|
||||
|
||||
const (
|
||||
LORAWAN_JOIN_TIMEOUT_SEC = 180
|
||||
LORAWAN_RECONNECT_DELAY_SEC = 15
|
||||
LORAWAN_UPLINK_DELAY_SEC = 60
|
||||
)
|
||||
|
||||
var (
|
||||
radio lora.Radio
|
||||
session *lorawan.Session
|
||||
otaa *lorawan.Otaa
|
||||
)
|
||||
|
||||
func loraConnect() error {
|
||||
start := time.Now()
|
||||
var err error
|
||||
for time.Since(start) < LORAWAN_JOIN_TIMEOUT_SEC*time.Second {
|
||||
println("Trying to join network")
|
||||
err = lorawan.Join(otaa, session)
|
||||
if err == nil {
|
||||
println("Connected to network !")
|
||||
return nil
|
||||
}
|
||||
println("Join error:", err, "retrying in", LORAWAN_RECONNECT_DELAY_SEC, "sec")
|
||||
time.Sleep(time.Second * LORAWAN_RECONNECT_DELAY_SEC)
|
||||
}
|
||||
|
||||
err = errors.New("Unable to join Lorawan network")
|
||||
println(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
func failMessage(err error) {
|
||||
println("FATAL:", err)
|
||||
for {
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
println("*** Lorawan basic join and uplink demo ***")
|
||||
|
||||
// Board specific Lorawan initialization
|
||||
var err error
|
||||
radio, err = common.SetupLora()
|
||||
if err != nil {
|
||||
failMessage(err)
|
||||
}
|
||||
|
||||
// Required for LoraWan operations
|
||||
session = &lorawan.Session{}
|
||||
otaa = &lorawan.Otaa{}
|
||||
|
||||
// Connect the lorawan with the Lora Radio device.
|
||||
lorawan.UseRadio(radio)
|
||||
|
||||
lorawan.UseRegionSettings(region.EU868())
|
||||
|
||||
// Configure AppEUI, DevEUI, APPKey, and public/private Lorawan Network
|
||||
setLorawanKeys()
|
||||
|
||||
if debug != "" {
|
||||
println("main: Network joined")
|
||||
println("main: DevEui, " + otaa.GetDevEUI())
|
||||
println("main: AppEui, " + otaa.GetAppEUI())
|
||||
println("main: DevAddr, " + otaa.GetAppKey())
|
||||
}
|
||||
|
||||
// Try to connect Lorawan network
|
||||
if err := loraConnect(); err != nil {
|
||||
failMessage(err)
|
||||
}
|
||||
|
||||
if debug != "" {
|
||||
println("main: NetID, " + otaa.GetNetID())
|
||||
println("main: NwkSKey, " + session.GetNwkSKey())
|
||||
println("main: AppSKey, " + session.GetAppSKey())
|
||||
println("main: Done")
|
||||
}
|
||||
// Try to periodicaly send an uplink sample message
|
||||
upCount := 1
|
||||
for {
|
||||
payload := "Hello TinyGo #" + strconv.Itoa(upCount)
|
||||
|
||||
if err := lorawan.SendUplink([]byte(payload), session); err != nil {
|
||||
println("Uplink error:", err)
|
||||
} else {
|
||||
println("Uplink success, msg=", payload)
|
||||
}
|
||||
|
||||
println("Sleeping for", LORAWAN_UPLINK_DELAY_SEC, "sec")
|
||||
time.Sleep(time.Second * LORAWAN_UPLINK_DELAY_SEC)
|
||||
upCount++
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
//go:build featherwing
|
||||
|
||||
package common
|
||||
|
||||
import "machine"
|
||||
|
||||
var (
|
||||
// We assume LoRa Featherwing module with sx127x is connected to PyBadge
|
||||
rstPin = machine.D11
|
||||
csPin = machine.D10
|
||||
dio0Pin = machine.D6
|
||||
dio1Pin = machine.D9
|
||||
spi = machine.SPI0
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
//go:build lgt92
|
||||
|
||||
package common
|
||||
|
||||
import "machine"
|
||||
|
||||
var (
|
||||
rstPin = machine.PB0
|
||||
csPin = machine.PA15
|
||||
dio0Pin = machine.PC13
|
||||
dio1Pin = machine.PB10
|
||||
spi = machine.SPI0
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
errRadioNotFound = errors.New("radio not found")
|
||||
errRxTimeout = errors.New("radio RX timeout")
|
||||
)
|
||||
@@ -1,9 +1,11 @@
|
||||
//go:build !featherwing && !gnse && !lorae5 && !nucleowl55jc
|
||||
//go:build !featherwing && !lgt92 && !stm32wlx && !sx126x
|
||||
|
||||
package main
|
||||
package common
|
||||
|
||||
import "tinygo.org/x/drivers/lora"
|
||||
|
||||
// do simulator setup here
|
||||
func setupLora() (LoraRadio, error) {
|
||||
func SetupLora() (lora.Radio, error) {
|
||||
return &SimLoraRadio{}, nil
|
||||
}
|
||||
|
||||
@@ -27,11 +29,12 @@ func (sr *SimLoraRadio) SetCodingRate(cr uint8) {}
|
||||
func (sr *SimLoraRadio) SetBandwidth(bw uint8) {}
|
||||
func (sr *SimLoraRadio) SetCrc(enable bool) {}
|
||||
func (sr *SimLoraRadio) SetSpreadingFactor(sf uint8) {}
|
||||
func (sr *SimLoraRadio) LoraConfig(cnf lora.Config) {}
|
||||
|
||||
func firmwareVersion() string {
|
||||
return "simulator " + currentVersion()
|
||||
func FirmwareVersion() string {
|
||||
return "simulator " + CurrentVersion()
|
||||
}
|
||||
|
||||
func lorarx() ([]byte, error) {
|
||||
func Lorarx() ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
//go:build stm32wlx
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"machine"
|
||||
|
||||
"tinygo.org/x/drivers/lora"
|
||||
"tinygo.org/x/drivers/sx126x"
|
||||
)
|
||||
|
||||
const (
|
||||
FREQ = 868100000
|
||||
LORA_DEFAULT_RXTIMEOUT_MS = 1000
|
||||
LORA_DEFAULT_TXTIMEOUT_MS = 5000
|
||||
)
|
||||
|
||||
var (
|
||||
loraRadio *sx126x.Device
|
||||
)
|
||||
|
||||
var spi = machine.SPI3
|
||||
|
||||
func newRadioControl() sx126x.RadioController {
|
||||
return sx126x.NewRadioControl()
|
||||
}
|
||||
|
||||
// do sx126x setup here
|
||||
func SetupLora() (lora.Radio, error) {
|
||||
loraRadio = sx126x.New(spi)
|
||||
loraRadio.SetDeviceType(sx126x.DEVICE_TYPE_SX1262)
|
||||
|
||||
// Create radio controller for target
|
||||
loraRadio.SetRadioController(newRadioControl())
|
||||
|
||||
if state := loraRadio.DetectDevice(); !state {
|
||||
return nil, errRadioNotFound
|
||||
}
|
||||
|
||||
return loraRadio, nil
|
||||
}
|
||||
|
||||
func FirmwareVersion() string {
|
||||
return "sx126x"
|
||||
}
|
||||
|
||||
func Lorarx() ([]byte, error) {
|
||||
return loraRadio.Rx(LORA_DEFAULT_RXTIMEOUT_MS)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
//go:build !stm32wlx && sx126x
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"machine"
|
||||
|
||||
"tinygo.org/x/drivers/lora"
|
||||
"tinygo.org/x/drivers/sx126x"
|
||||
)
|
||||
|
||||
const (
|
||||
FREQ = 868100000
|
||||
LORA_DEFAULT_RXTIMEOUT_MS = 1000
|
||||
LORA_DEFAULT_TXTIMEOUT_MS = 5000
|
||||
)
|
||||
|
||||
var (
|
||||
loraRadio *sx126x.Device
|
||||
)
|
||||
|
||||
var (
|
||||
spi = machine.SPI0
|
||||
nssPin, busyPin, dio1Pin = machine.GP17, machine.GP10, machine.GP11
|
||||
rxPin, txLowPin, txHighPin = machine.GP13, machine.GP12, machine.GP12
|
||||
)
|
||||
|
||||
func newRadioControl() sx126x.RadioController {
|
||||
return sx126x.NewRadioControl(nssPin, busyPin, dio1Pin, rxPin, txLowPin, txHighPin)
|
||||
}
|
||||
|
||||
// do sx126x setup here
|
||||
func SetupLora() (lora.Radio, error) {
|
||||
loraRadio = sx126x.New(spi)
|
||||
loraRadio.SetDeviceType(sx126x.DEVICE_TYPE_SX1262)
|
||||
|
||||
// Create radio controller for target
|
||||
loraRadio.SetRadioController(newRadioControl())
|
||||
|
||||
if state := loraRadio.DetectDevice(); !state {
|
||||
return nil, errRadioNotFound
|
||||
}
|
||||
|
||||
return loraRadio, nil
|
||||
}
|
||||
|
||||
func FirmwareVersion() string {
|
||||
return "sx126x"
|
||||
}
|
||||
|
||||
func Lorarx() ([]byte, error) {
|
||||
return loraRadio.Rx(LORA_DEFAULT_RXTIMEOUT_MS)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
//go:build featherwing || lgt92
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"machine"
|
||||
|
||||
"tinygo.org/x/drivers/lora"
|
||||
"tinygo.org/x/drivers/sx127x"
|
||||
)
|
||||
|
||||
const (
|
||||
FREQ = 868100000
|
||||
LORA_DEFAULT_RXTIMEOUT_MS = 1000
|
||||
LORA_DEFAULT_TXTIMEOUT_MS = 5000
|
||||
)
|
||||
|
||||
var (
|
||||
loraRadio *sx127x.Device
|
||||
)
|
||||
|
||||
// do sx127x setup here
|
||||
func SetupLora() (lora.Radio, error) {
|
||||
rstPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
spi.Configure(machine.SPIConfig{Frequency: 500000, Mode: 0})
|
||||
|
||||
loraRadio = sx127x.New(spi, rstPin)
|
||||
loraRadio.SetRadioController(sx127x.NewRadioControl(csPin, dio0Pin, dio1Pin))
|
||||
loraRadio.Reset()
|
||||
|
||||
if state := loraRadio.DetectDevice(); !state {
|
||||
return nil, errRadioNotFound
|
||||
}
|
||||
|
||||
return loraRadio, nil
|
||||
}
|
||||
|
||||
func FirmwareVersion() string {
|
||||
v := loraRadio.GetVersion()
|
||||
return "sx127x v" + strconv.Itoa(int(v))
|
||||
}
|
||||
|
||||
func Lorarx() ([]byte, error) {
|
||||
return loraRadio.Rx(LORA_DEFAULT_RXTIMEOUT_MS)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package common
|
||||
|
||||
const VERSION = "0.0.1"
|
||||
|
||||
func CurrentVersion() string {
|
||||
return VERSION
|
||||
}
|
||||
@@ -5,8 +5,6 @@ import (
|
||||
"machine"
|
||||
"time"
|
||||
|
||||
rfswitch "tinygo.org/x/drivers/examples/sx126x/rfswitch"
|
||||
|
||||
"tinygo.org/x/drivers/lora"
|
||||
"tinygo.org/x/drivers/sx126x"
|
||||
)
|
||||
@@ -24,12 +22,11 @@ func main() {
|
||||
machine.LED.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
|
||||
// Create the driver
|
||||
loraRadio = sx126x.New(machine.SPI3)
|
||||
loraRadio = sx126x.New(spi)
|
||||
loraRadio.SetDeviceType(sx126x.DEVICE_TYPE_SX1262)
|
||||
|
||||
// Create RF Switch
|
||||
var radioSwitch rfswitch.CustomSwitch
|
||||
loraRadio.SetRfSwitch(radioSwitch)
|
||||
// Create radio controller for target
|
||||
loraRadio.SetRadioController(newRadioControl())
|
||||
|
||||
state := loraRadio.DetectDevice()
|
||||
if !state {
|
||||
@@ -38,8 +35,8 @@ func main() {
|
||||
|
||||
// Prepare for Lora operation
|
||||
loraConf := lora.Config{
|
||||
Freq: FREQ,
|
||||
Bw: lora.Bandwidth_500_0,
|
||||
Freq: lora.MHz_868_1,
|
||||
Bw: lora.Bandwidth_125_0,
|
||||
Sf: lora.SpreadingFactor9,
|
||||
Cr: lora.CodingRate4_7,
|
||||
HeaderType: lora.HeaderExplicit,
|
||||
@@ -48,7 +45,7 @@ func main() {
|
||||
Iq: lora.IQStandard,
|
||||
Crc: lora.CRCOn,
|
||||
SyncWord: lora.SyncPrivate,
|
||||
LoraTxPowerDBm: 14,
|
||||
LoraTxPowerDBm: 20,
|
||||
}
|
||||
loraRadio.LoraConfig(loraConf)
|
||||
|
||||
@@ -76,6 +73,5 @@ func main() {
|
||||
|
||||
loraRadio.SetStandby()
|
||||
time.Sleep(60 * time.Second)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
//go:build !stm32wlx
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"machine"
|
||||
|
||||
"tinygo.org/x/drivers/sx126x"
|
||||
)
|
||||
|
||||
var (
|
||||
spi = machine.SPI0
|
||||
nssPin, busyPin, dio1Pin = machine.GP17, machine.GP10, machine.GP11
|
||||
rxPin, txLowPin, txHighPin = machine.GP13, machine.GP12, machine.GP12
|
||||
)
|
||||
|
||||
func newRadioControl() sx126x.RadioController {
|
||||
return sx126x.NewRadioControl(nssPin, busyPin, dio1Pin, rxPin, txLowPin, txHighPin)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//go:build stm32wlx
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"machine"
|
||||
|
||||
"tinygo.org/x/drivers/sx126x"
|
||||
)
|
||||
|
||||
var spi = machine.SPI3
|
||||
|
||||
func newRadioControl() sx126x.RadioController {
|
||||
return sx126x.NewRadioControl()
|
||||
}
|
||||
@@ -4,19 +4,13 @@ package main
|
||||
// module will be in RX mode between two transmissions
|
||||
|
||||
import (
|
||||
"device/stm32"
|
||||
"machine"
|
||||
"runtime/interrupt"
|
||||
"time"
|
||||
|
||||
rfswitch "tinygo.org/x/drivers/examples/sx126x/rfswitch"
|
||||
|
||||
"tinygo.org/x/drivers/lora"
|
||||
"tinygo.org/x/drivers/sx126x"
|
||||
)
|
||||
|
||||
const FREQ = 868100000
|
||||
|
||||
const (
|
||||
LORA_DEFAULT_RXTIMEOUT_MS = 1000
|
||||
LORA_DEFAULT_TXTIMEOUT_MS = 5000
|
||||
@@ -27,23 +21,19 @@ var (
|
||||
txmsg = []byte("Hello TinyGO")
|
||||
)
|
||||
|
||||
// radioIntHandler will take care of radio interrupts
|
||||
func radioIntHandler(intr interrupt.Interrupt) {
|
||||
loraRadio.HandleInterrupt()
|
||||
}
|
||||
|
||||
func main() {
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
println("\n# TinyGo Lora RX/TX test")
|
||||
println("# ----------------------")
|
||||
machine.LED.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
|
||||
// Create the driver
|
||||
loraRadio = sx126x.New(machine.SPI3)
|
||||
loraRadio = sx126x.New(spi)
|
||||
loraRadio.SetDeviceType(sx126x.DEVICE_TYPE_SX1262)
|
||||
|
||||
// Create RF Switch
|
||||
var radioSwitch rfswitch.CustomSwitch
|
||||
loraRadio.SetRfSwitch(radioSwitch)
|
||||
// Create radio controller for target
|
||||
loraRadio.SetRadioController(newRadioControl())
|
||||
|
||||
// Detect the device
|
||||
state := loraRadio.DetectDevice()
|
||||
@@ -51,13 +41,9 @@ func main() {
|
||||
panic("sx126x not detected.")
|
||||
}
|
||||
|
||||
// Add interrupt handler for Radio IRQs
|
||||
intr := interrupt.New(stm32.IRQ_Radio_IRQ_Busy, radioIntHandler)
|
||||
intr.Enable()
|
||||
|
||||
loraConf := lora.Config{
|
||||
Freq: FREQ,
|
||||
Bw: lora.Bandwidth_500_0,
|
||||
Freq: lora.MHz_868_1,
|
||||
Bw: lora.Bandwidth_125_0,
|
||||
Sf: lora.SpreadingFactor9,
|
||||
Cr: lora.CodingRate4_7,
|
||||
HeaderType: lora.HeaderExplicit,
|
||||
@@ -73,10 +59,10 @@ func main() {
|
||||
|
||||
var count uint
|
||||
for {
|
||||
tStart := time.Now()
|
||||
start := time.Now()
|
||||
|
||||
println("main: Receiving Lora for 10 seconds")
|
||||
for int(time.Now().Sub(tStart).Seconds()) < 10 {
|
||||
for time.Since(start) < 10*time.Second {
|
||||
buf, err := loraRadio.Rx(LORA_DEFAULT_RXTIMEOUT_MS)
|
||||
if err != nil {
|
||||
println("RX Error: ", err)
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
//go:build !stm32wlx
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"machine"
|
||||
|
||||
"tinygo.org/x/drivers/sx126x"
|
||||
)
|
||||
|
||||
var (
|
||||
spi = machine.SPI1
|
||||
nssPin, busyPin, dio1Pin = machine.GP13, machine.GP6, machine.GP7
|
||||
rxPin, txLowPin, txHighPin = machine.GP9, machine.GP8, machine.GP8
|
||||
)
|
||||
|
||||
func newRadioControl() sx126x.RadioController {
|
||||
return sx126x.NewRadioControl(nssPin, busyPin, dio1Pin, rxPin, txLowPin, txHighPin)
|
||||
}
|
||||
|
||||
func init() {
|
||||
spi.Configure(machine.SPIConfig{
|
||||
Mode: 0,
|
||||
Frequency: 8 * 1e6,
|
||||
SDO: machine.SPI1_SDO_PIN,
|
||||
SDI: machine.SPI1_SDI_PIN,
|
||||
SCK: machine.SPI1_SCK_PIN,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//go:build stm32wlx
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"machine"
|
||||
|
||||
"tinygo.org/x/drivers/sx126x"
|
||||
)
|
||||
|
||||
var spi = machine.SPI3
|
||||
|
||||
func newRadioControl() sx126x.RadioController {
|
||||
return sx126x.NewRadioControl()
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
//go:build gnse
|
||||
|
||||
/*
|
||||
Generic Node Sensor Edition
|
||||
RFSwitch
|
||||
|
||||
Disable Switch : PB8=OFF PA0=OFF PA1=OFF
|
||||
Enable RX : PB8=ON PA0=ON PA1=OFF
|
||||
Enable TX RFO LP : PB8=ON PA0=ON PA1=ON
|
||||
Enable TX RFO HP : PB8=ON PA0=OFF PA1=ON
|
||||
*/
|
||||
package rfswitch
|
||||
|
||||
import (
|
||||
"machine"
|
||||
|
||||
"tinygo.org/x/drivers/sx126x"
|
||||
)
|
||||
|
||||
type CustomSwitch struct {
|
||||
}
|
||||
|
||||
var (
|
||||
rfstate int
|
||||
)
|
||||
|
||||
func (s CustomSwitch) InitRFSwitch() {
|
||||
machine.RF_FE_CTRL1.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
machine.RF_FE_CTRL2.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
machine.RF_FE_CTRL3.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
rfstate = -1 //Unknown
|
||||
}
|
||||
|
||||
func (s CustomSwitch) SetRfSwitchMode(mode int) error {
|
||||
if mode == rfstate {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch mode {
|
||||
|
||||
case sx126x.RFSWITCH_TX_HP:
|
||||
machine.RF_FE_CTRL1.Set(false)
|
||||
machine.RF_FE_CTRL2.Set(true)
|
||||
machine.RF_FE_CTRL3.Set(true)
|
||||
|
||||
case sx126x.RFSWITCH_TX_LP:
|
||||
machine.RF_FE_CTRL1.Set(true)
|
||||
machine.RF_FE_CTRL2.Set(true)
|
||||
machine.RF_FE_CTRL3.Set(true)
|
||||
|
||||
case sx126x.RFSWITCH_RX:
|
||||
machine.RF_FE_CTRL1.Set(true)
|
||||
machine.RF_FE_CTRL2.Set(false)
|
||||
machine.RF_FE_CTRL3.Set(true)
|
||||
}
|
||||
|
||||
rfstate = mode
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
//go:build lorae5
|
||||
|
||||
package radio
|
||||
|
||||
/*
|
||||
/!\ LoRa-E5 module ONLY transmits through RFO_HP:
|
||||
|
||||
Receive: PA4=1, PA5=0
|
||||
Transmit(high output power, SMPS mode): PA4=0, PA5=1
|
||||
|
||||
*/
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"machine"
|
||||
|
||||
"tinygo.org/x/drivers/sx126x"
|
||||
)
|
||||
|
||||
type CustomSwitch struct {
|
||||
}
|
||||
|
||||
func (s CustomSwitch) InitRFSwitch() {
|
||||
machine.PA4.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
machine.PB5.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
}
|
||||
|
||||
func (s CustomSwitch) SetRfSwitchMode(mode int) error {
|
||||
switch mode {
|
||||
|
||||
case sx126x.RFSWITCH_RX:
|
||||
machine.PA4.Set(true)
|
||||
machine.PB5.Set(false)
|
||||
case sx126x.RFSWITCH_TX_LP:
|
||||
errors.New("RFSWITCH_TX_LP not supported ")
|
||||
case sx126x.RFSWITCH_TX_HP:
|
||||
machine.PA4.Set(false)
|
||||
machine.PB5.Set(true)
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
//go:build nucleowl55jc
|
||||
|
||||
/*
|
||||
Nucleo WL55JC1
|
||||
RFSwitch
|
||||
|
||||
+-----------+---------+------------+------------+
|
||||
| | FE_CTRL1 | FE_CTRL2 | FE_CTRL3 |
|
||||
| | (PC4) | (PC5) | (PC3) |
|
||||
+-----------+----------+-----------+------------+
|
||||
| TX_HP | LOW | HIGH | HIGH |
|
||||
| TX_LP | HIGH | HIGH | HIGH |
|
||||
| RX | HIGH | LOW | HIGH |
|
||||
+-----------+----------+-----------+------------+
|
||||
*/
|
||||
package rfswitch
|
||||
|
||||
import (
|
||||
"machine"
|
||||
|
||||
"tinygo.org/x/drivers/sx126x"
|
||||
)
|
||||
|
||||
type CustomSwitch struct {
|
||||
}
|
||||
|
||||
func (s CustomSwitch) InitRFSwitch() {
|
||||
machine.PC4.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
machine.PC5.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
machine.PC3.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
}
|
||||
|
||||
func (s CustomSwitch) SetRfSwitchMode(mode int) error {
|
||||
switch mode {
|
||||
|
||||
case sx126x.RFSWITCH_TX_HP:
|
||||
machine.PC4.Set(false)
|
||||
machine.PC5.Set(true)
|
||||
machine.PC3.Set(true)
|
||||
|
||||
case sx126x.RFSWITCH_TX_LP:
|
||||
machine.PC4.Set(true)
|
||||
machine.PC5.Set(true)
|
||||
machine.PC3.Set(true)
|
||||
|
||||
case sx126x.RFSWITCH_RX:
|
||||
machine.PC4.Set(true)
|
||||
machine.PC5.Set(false)
|
||||
machine.PC3.Set(true)
|
||||
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
@@ -11,8 +11,6 @@ import (
|
||||
"tinygo.org/x/drivers/sx127x"
|
||||
)
|
||||
|
||||
const FREQ = 868100000
|
||||
|
||||
const (
|
||||
LORA_DEFAULT_RXTIMEOUT_MS = 1000
|
||||
LORA_DEFAULT_TXTIMEOUT_MS = 5000
|
||||
@@ -40,13 +38,13 @@ func main() {
|
||||
println("# ----------------------")
|
||||
machine.LED.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
SX127X_PIN_RST.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
SX127X_PIN_CS.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
SX127X_PIN_DIO0.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
|
||||
SX127X_PIN_DIO1.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
|
||||
|
||||
SX127X_SPI.Configure(machine.SPIConfig{Frequency: 500000, Mode: 0})
|
||||
|
||||
println("main: create and start SX127x driver")
|
||||
loraRadio = sx127x.New(SX127X_SPI, SX127X_PIN_CS, SX127X_PIN_RST)
|
||||
loraRadio = sx127x.New(SX127X_SPI, SX127X_PIN_RST)
|
||||
loraRadio.SetRadioController(sx127x.NewRadioControl(SX127X_PIN_CS, SX127X_PIN_DIO0, SX127X_PIN_DIO1))
|
||||
|
||||
loraRadio.Reset()
|
||||
state := loraRadio.DetectDevice()
|
||||
if !state {
|
||||
@@ -55,20 +53,10 @@ func main() {
|
||||
println("main: sx127x found")
|
||||
}
|
||||
|
||||
// Setup DIO0 interrupt Handling
|
||||
if err := SX127X_PIN_DIO0.SetInterrupt(machine.PinRising, dioIrqHandler); err != nil {
|
||||
println("could not configure DIO0 pin interrupt:", err.Error())
|
||||
}
|
||||
|
||||
// Setup DIO1 interrupt Handling
|
||||
if err := SX127X_PIN_DIO1.SetInterrupt(machine.PinRising, dioIrqHandler); err != nil {
|
||||
println("could not configure DIO1 pin interrupt:", err.Error())
|
||||
}
|
||||
|
||||
// Prepare for Lora Operation
|
||||
loraConf := lora.Config{
|
||||
Freq: FREQ,
|
||||
Bw: lora.Bandwidth_500_0,
|
||||
Freq: lora.MHz_868_1,
|
||||
Bw: lora.Bandwidth_125_0,
|
||||
Sf: lora.SpreadingFactor9,
|
||||
Cr: lora.CodingRate4_7,
|
||||
HeaderType: lora.HeaderExplicit,
|
||||
|
||||
+46
-6
@@ -13,6 +13,39 @@ import (
|
||||
var (
|
||||
errInvalidNMEASentenceLength = errors.New("invalid NMEA sentence length")
|
||||
errInvalidNMEAChecksum = errors.New("invalid NMEA sentence checksum")
|
||||
errEmptyNMEASentence = errors.New("cannot parse empty NMEA sentence")
|
||||
errUnknownNMEASentence = errors.New("unsupported NMEA sentence type")
|
||||
errInvalidGGASentence = errors.New("invalid GGA NMEA sentence")
|
||||
errInvalidRMCSentence = errors.New("invalid RMC NMEA sentence")
|
||||
errInvalidGLLSentence = errors.New("invalid GLL NMEA sentence")
|
||||
)
|
||||
|
||||
type GPSError struct {
|
||||
Err error
|
||||
Info string
|
||||
Sentence string
|
||||
}
|
||||
|
||||
func newGPSError(err error, sentence string, info string) GPSError {
|
||||
return GPSError{
|
||||
Info: info,
|
||||
Err: err,
|
||||
Sentence: sentence,
|
||||
}
|
||||
}
|
||||
|
||||
func (ge GPSError) Error() string {
|
||||
return ge.Err.Error() + " " + ge.Info + " " + ge.Sentence
|
||||
}
|
||||
|
||||
func (ge GPSError) Unwrap() error {
|
||||
return ge.Err
|
||||
}
|
||||
|
||||
const (
|
||||
minimumNMEALength = 7
|
||||
startingDelimiter = '$'
|
||||
checksumDelimiter = '*'
|
||||
)
|
||||
|
||||
// Device wraps a connection to a GPS device.
|
||||
@@ -60,11 +93,11 @@ func (gps *Device) readNextSentence() (sentence string) {
|
||||
gps.sentence.Reset()
|
||||
var b byte = ' '
|
||||
|
||||
for b != '$' {
|
||||
for b != startingDelimiter {
|
||||
b = gps.readNextByte()
|
||||
}
|
||||
|
||||
for b != '*' {
|
||||
for b != checksumDelimiter {
|
||||
gps.sentence.WriteByte(b)
|
||||
b = gps.readNextByte()
|
||||
}
|
||||
@@ -126,17 +159,24 @@ func (gps *Device) WriteBytes(bytes []byte) {
|
||||
}
|
||||
|
||||
// validSentence checks if a sentence has been received uncorrupted
|
||||
// For example, a valid NMEA sentence such as this:
|
||||
// $GPGLL,3751.65,S,14507.36,E*77
|
||||
// It has to start with a '$' character.
|
||||
// It has to have a 5 character long sentence identifier.
|
||||
// It has to end with a '*' character following by a checksum.
|
||||
func validSentence(sentence string) error {
|
||||
if len(sentence) < 4 || sentence[0] != '$' || sentence[len(sentence)-3] != '*' {
|
||||
if len(sentence) < minimumNMEALength || sentence[0] != startingDelimiter || sentence[len(sentence)-3] != checksumDelimiter {
|
||||
return errInvalidNMEASentenceLength
|
||||
}
|
||||
var cs byte = 0
|
||||
for i := 1; i < len(sentence)-3; i++ {
|
||||
cs ^= sentence[i]
|
||||
}
|
||||
checksum := hex.EncodeToString([]byte{cs})
|
||||
if (checksum[0] != sentence[len(sentence)-2]) || (checksum[1] != sentence[len(sentence)-1]) {
|
||||
return errInvalidNMEAChecksum
|
||||
checksum := strings.ToUpper(hex.EncodeToString([]byte{cs}))
|
||||
if checksum != sentence[len(sentence)-2:len(sentence)] {
|
||||
return newGPSError(errInvalidNMEAChecksum, sentence,
|
||||
"expected "+sentence[len(sentence)-2:len(sentence)]+
|
||||
" got "+checksum)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
+47
-32
@@ -1,19 +1,11 @@
|
||||
package gps
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
errEmptyNMEASentence = errors.New("cannot parse empty NMEA sentence")
|
||||
errUnknownNMEASentence = errors.New("unsupported NMEA sentence type")
|
||||
errInvalidGGASentence = errors.New("invalid GGA NMEA sentence")
|
||||
errInvalidRMCSentence = errors.New("invalid RMC NMEA sentence")
|
||||
)
|
||||
|
||||
// Parser for GPS NMEA sentences.
|
||||
type Parser struct {
|
||||
}
|
||||
@@ -51,43 +43,63 @@ func NewParser() Parser {
|
||||
}
|
||||
|
||||
// Parse parses a NMEA sentence looking for fix info.
|
||||
func (parser *Parser) Parse(sentence string) (fix Fix, err error) {
|
||||
func (parser *Parser) Parse(sentence string) (Fix, error) {
|
||||
var fix Fix
|
||||
if sentence == "" {
|
||||
err = errEmptyNMEASentence
|
||||
return
|
||||
return fix, errEmptyNMEASentence
|
||||
}
|
||||
if len(sentence) < 6 {
|
||||
return fix, errInvalidNMEASentenceLength
|
||||
}
|
||||
typ := sentence[3:6]
|
||||
switch typ {
|
||||
case "GGA":
|
||||
// https://docs.novatel.com/OEM7/Content/Logs/GPGGA.htm
|
||||
fields := strings.Split(sentence, ",")
|
||||
if len(fields) != 15 {
|
||||
err = errInvalidGGASentence
|
||||
return
|
||||
return fix, errInvalidGGASentence
|
||||
}
|
||||
|
||||
fix.Altitude = findAltitude(fields[9])
|
||||
fix.Satellites = findSatellites(fields[7])
|
||||
fix.Longitude = findLongitude(fields[4], fields[5])
|
||||
fix.Latitude = findLatitude(fields[2], fields[3])
|
||||
fix.Time = findTime(fields[1])
|
||||
fix.Latitude = findLatitude(fields[2], fields[3])
|
||||
fix.Longitude = findLongitude(fields[4], fields[5])
|
||||
fix.Satellites = findSatellites(fields[7])
|
||||
fix.Altitude = findAltitude(fields[9])
|
||||
fix.Valid = (fix.Altitude != -99999) && (fix.Satellites > 0)
|
||||
|
||||
return fix, nil
|
||||
case "GLL":
|
||||
// https://docs.novatel.com/OEM7/Content/Logs/GPGLL.htm
|
||||
fields := strings.Split(sentence, ",")
|
||||
if len(fields) != 8 {
|
||||
return fix, errInvalidGLLSentence
|
||||
}
|
||||
|
||||
fix.Latitude = findLatitude(fields[1], fields[2])
|
||||
fix.Longitude = findLongitude(fields[3], fields[4])
|
||||
fix.Time = findTime(fields[5])
|
||||
|
||||
fix.Valid = (fields[6] == "A")
|
||||
|
||||
return fix, nil
|
||||
case "RMC":
|
||||
// https://docs.novatel.com/OEM7/Content/Logs/GPRMC.htm
|
||||
fields := strings.Split(sentence, ",")
|
||||
if len(fields) != 13 {
|
||||
err = errInvalidRMCSentence
|
||||
return
|
||||
return fix, errInvalidRMCSentence
|
||||
}
|
||||
|
||||
fix.Longitude = findLongitude(fields[5], fields[6])
|
||||
fix.Latitude = findLatitude(fields[3], fields[4])
|
||||
fix.Time = findTime(fields[1])
|
||||
fix.Valid = (fields[2] == "A")
|
||||
fix.Latitude = findLatitude(fields[3], fields[4])
|
||||
fix.Longitude = findLongitude(fields[5], fields[6])
|
||||
fix.Speed = findSpeed(fields[7])
|
||||
fix.Heading = findHeading(fields[8])
|
||||
fix.Valid = (len(fields[2]) > 0 && fields[2][0:1] == "A")
|
||||
default:
|
||||
err = errUnknownNMEASentence
|
||||
|
||||
return fix, nil
|
||||
}
|
||||
return
|
||||
|
||||
return fix, newGPSError(errUnknownNMEASentence, sentence, typ)
|
||||
}
|
||||
|
||||
// findTime returns the time from an NMEA sentence:
|
||||
@@ -100,7 +112,10 @@ func findTime(val string) time.Time {
|
||||
h, _ := strconv.ParseInt(val[0:2], 10, 8)
|
||||
m, _ := strconv.ParseInt(val[2:4], 10, 8)
|
||||
s, _ := strconv.ParseInt(val[4:6], 10, 8)
|
||||
ms, _ := strconv.ParseInt(val[7:10], 10, 16)
|
||||
ms := int64(0)
|
||||
if len(val) > 6 {
|
||||
ms, _ = strconv.ParseInt(val[7:], 10, 16)
|
||||
}
|
||||
t := time.Date(0, 0, 0, int(h), int(m), int(s), int(ms), time.UTC)
|
||||
|
||||
return t
|
||||
@@ -120,11 +135,11 @@ func findAltitude(val string) int32 {
|
||||
// $--GGA,,ddmm.mmmmm,x,,,,,,,,,,,*hh
|
||||
func findLatitude(val, hemi string) float32 {
|
||||
if len(val) > 8 {
|
||||
var dd = val[0:2]
|
||||
var mm = val[2:]
|
||||
var d, _ = strconv.ParseFloat(dd, 32)
|
||||
var m, _ = strconv.ParseFloat(mm, 32)
|
||||
var v = float32(d + (m / 60))
|
||||
dd := val[0:2]
|
||||
mm := val[2:]
|
||||
d, _ := strconv.ParseFloat(dd, 32)
|
||||
m, _ := strconv.ParseFloat(mm, 32)
|
||||
v := float32(d + (m / 60))
|
||||
if hemi == "S" {
|
||||
v *= -1
|
||||
}
|
||||
@@ -133,7 +148,7 @@ func findLatitude(val, hemi string) float32 {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
// findLatitude returns the longitude from an NMEA sentence:
|
||||
// findLongitude returns the longitude from an NMEA sentence:
|
||||
// $--GGA,,,,dddmm.mmmmm,x,,,,,,,,,*hh
|
||||
func findLongitude(val, hemi string) float32 {
|
||||
if len(val) > 8 {
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package gps
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestParseUnknownSentence(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
p := NewParser()
|
||||
|
||||
val := "$GPGSV,3,1,09,07,14,317,22,08,31,284,25,10,32,133,39,16,85,232,29*7F"
|
||||
_, err := p.Parse(val)
|
||||
c.Assert(err.Error(), qt.Contains, "unsupported NMEA sentence type")
|
||||
}
|
||||
|
||||
func TestParseGGA(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
p := NewParser()
|
||||
|
||||
val := "$GPGGA,115739.00,4158.8441367,N,09147.4416929,"
|
||||
fix, err := p.Parse(val)
|
||||
if err != errInvalidGGASentence {
|
||||
t.Error("should have errInvalidGGASentence error")
|
||||
}
|
||||
|
||||
val = "$GPGGA,115739.00,4158.8441367,N,09147.4416929,W,4,13,0.9,255.747,M,-32.00,M,01,0000*6E"
|
||||
fix, err = p.Parse(val)
|
||||
if err != nil {
|
||||
t.Error("should have parsed")
|
||||
}
|
||||
c.Assert(fix.Latitude, qt.Equals, float32(41.980735778808594))
|
||||
c.Assert(fix.Longitude, qt.Equals, float32(-91.79069519042969))
|
||||
c.Assert(fix.Altitude, qt.Equals, int32(255))
|
||||
}
|
||||
|
||||
func TestParseGLL(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
p := NewParser()
|
||||
|
||||
val := "$GPGLL,3953.88008971,N,10506.7531891"
|
||||
_, err := p.Parse(val)
|
||||
if err != errInvalidGLLSentence {
|
||||
t.Error("should have errInvalidGLLSentence error")
|
||||
}
|
||||
|
||||
val = "$GPGLL,5109.0262317,N,11401.8407304,W,202725.00,A,D*79"
|
||||
fix, err := p.Parse(val)
|
||||
if err != nil {
|
||||
t.Error("should have parsed")
|
||||
}
|
||||
|
||||
c.Assert(fix.Latitude, qt.Equals, float32(51.15043640136719))
|
||||
c.Assert(fix.Longitude, qt.Equals, float32(-114.03067779541016))
|
||||
}
|
||||
|
||||
func TestParseRMC(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
p := NewParser()
|
||||
|
||||
val := "$GPRMC,203522.00,A,5109.0262308,N,11401.8407342,"
|
||||
_, err := p.Parse(val)
|
||||
if err != errInvalidRMCSentence {
|
||||
t.Error("should have errInvalidRMCSentence error")
|
||||
}
|
||||
|
||||
val = "$GPRMC,203522.00,A,5109.0262308,N,11401.8407342,W,0.004,133.4,130522,0.0,E,D*2B"
|
||||
fix, err := p.Parse(val)
|
||||
if err != nil {
|
||||
t.Error("should have parsed")
|
||||
}
|
||||
|
||||
c.Assert(fix.Latitude, qt.Equals, float32(51.15043640136719))
|
||||
c.Assert(fix.Longitude, qt.Equals, float32(-114.03067779541016))
|
||||
}
|
||||
|
||||
func TestTime(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
val := ""
|
||||
tm := findTime(val)
|
||||
c.Assert(tm, qt.Equals, time.Time{})
|
||||
|
||||
val = "225446"
|
||||
tm = findTime(val)
|
||||
c.Assert(tm, qt.Equals, time.Date(0, 0, 0, 22, 54, 46, 0, time.UTC))
|
||||
|
||||
val = "124326.02752"
|
||||
tm = findTime(val)
|
||||
c.Assert(tm, qt.Equals, time.Date(0, 0, 0, 12, 43, 26, 2752, time.UTC))
|
||||
}
|
||||
@@ -76,3 +76,10 @@ const (
|
||||
SyncPublic = iota
|
||||
SyncPrivate
|
||||
)
|
||||
|
||||
const (
|
||||
MHz_868_1 = 868100000
|
||||
MHz_868_5 = 868500000
|
||||
MHz_916_8 = 916800000
|
||||
MHz_923_3 = 923300000
|
||||
)
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
package lorawan
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"tinygo.org/x/drivers/lora"
|
||||
"tinygo.org/x/drivers/lora/lorawan/region"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoJoinAcceptReceived = errors.New("no JoinAccept packet received")
|
||||
ErrNoRadioAttached = errors.New("no LoRa radio attached")
|
||||
ErrInvalidEuiLength = errors.New("invalid EUI length")
|
||||
ErrInvalidAppKeyLength = errors.New("invalid AppKey length")
|
||||
ErrInvalidPacketLength = errors.New("invalid packet length")
|
||||
ErrInvalidDevAddrLength = errors.New("invalid DevAddr length")
|
||||
ErrInvalidMic = errors.New("invalid Mic")
|
||||
ErrFrmPayloadTooLarge = errors.New("FRM payload too large")
|
||||
ErrInvalidNetIDLength = errors.New("invalid NetID length")
|
||||
ErrInvalidNwkSKeyLength = errors.New("invalid NwkSKey length")
|
||||
ErrInvalidAppSKeyLength = errors.New("invalid AppSKey length")
|
||||
ErrUndefinedRegionSettings = errors.New("undefined Regionnal Settings ")
|
||||
)
|
||||
|
||||
const (
|
||||
LORA_TX_TIMEOUT = 2000
|
||||
LORA_RX_TIMEOUT = 10000
|
||||
)
|
||||
|
||||
var (
|
||||
ActiveRadio lora.Radio
|
||||
Retries = 15
|
||||
regionSettings region.RegionSettings
|
||||
)
|
||||
|
||||
// UseRegionSettings sets current Lorawan Regional parameters
|
||||
func UseRegionSettings(rs region.RegionSettings) {
|
||||
regionSettings = rs
|
||||
}
|
||||
|
||||
// UseRadio attaches Lora radio driver to Lorawan
|
||||
func UseRadio(r lora.Radio) {
|
||||
if ActiveRadio != nil {
|
||||
panic("lorawan.ActiveRadio is already set")
|
||||
}
|
||||
ActiveRadio = r
|
||||
}
|
||||
|
||||
// SetPublicNetwork defines Lora Sync Word according to network type (public/private)
|
||||
func SetPublicNetwork(enabled bool) {
|
||||
ActiveRadio.SetPublicNetwork(enabled)
|
||||
}
|
||||
|
||||
// ApplyChannelConfig sets current Lora modulation according to current regional settings
|
||||
func applyChannelConfig(ch *region.Channel) {
|
||||
ActiveRadio.SetFrequency(ch.Frequency)
|
||||
ActiveRadio.SetBandwidth(ch.Bandwidth)
|
||||
ActiveRadio.SetCodingRate(ch.CodingRate)
|
||||
ActiveRadio.SetSpreadingFactor(ch.SpreadingFactor)
|
||||
ActiveRadio.SetPreambleLength(ch.PreambleLength)
|
||||
ActiveRadio.SetTxPower(ch.TxPowerDBm)
|
||||
// Lorawan defaults to explicit headers
|
||||
ActiveRadio.SetHeaderType(lora.HeaderExplicit)
|
||||
ActiveRadio.SetCrc(true)
|
||||
}
|
||||
|
||||
// Join tries to connect Lorawan Gateway
|
||||
func Join(otaa *Otaa, session *Session) error {
|
||||
var resp []uint8
|
||||
|
||||
if ActiveRadio == nil {
|
||||
return ErrNoRadioAttached
|
||||
}
|
||||
|
||||
if regionSettings == nil {
|
||||
return ErrUndefinedRegionSettings
|
||||
}
|
||||
|
||||
otaa.Init()
|
||||
|
||||
// Send join packet
|
||||
payload, err := otaa.GenerateJoinRequest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Prepare radio for Join Tx
|
||||
applyChannelConfig(regionSettings.JoinRequestChannel())
|
||||
ActiveRadio.SetIqMode(lora.IQStandard)
|
||||
ActiveRadio.Tx(payload, LORA_TX_TIMEOUT)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait for JoinAccept
|
||||
applyChannelConfig(regionSettings.JoinAcceptChannel())
|
||||
ActiveRadio.SetIqMode(lora.IQInverted)
|
||||
resp, err = ActiveRadio.Rx(LORA_RX_TIMEOUT)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp == nil {
|
||||
return ErrNoJoinAcceptReceived
|
||||
}
|
||||
|
||||
err = otaa.DecodeJoinAccept(resp, session)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendUplink sends Lorawan Uplink message
|
||||
func SendUplink(data []uint8, session *Session) error {
|
||||
|
||||
if regionSettings == nil {
|
||||
return ErrUndefinedRegionSettings
|
||||
}
|
||||
|
||||
payload, err := session.GenMessage(0, []byte(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
applyChannelConfig(regionSettings.UplinkChannel())
|
||||
ActiveRadio.SetIqMode(lora.IQStandard)
|
||||
ActiveRadio.Tx(payload, LORA_TX_TIMEOUT)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ListenDownlink() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package lorawan
|
||||
|
||||
import "crypto/rand"
|
||||
|
||||
// reverseBytes reverses order of a given byte slice
|
||||
func reverseBytes(s []byte) []byte {
|
||||
result := make([]byte, len(s))
|
||||
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
|
||||
result[i], result[j] = s[j], s[i]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetRand16 returns 2 random bytes
|
||||
func GetRand16() ([2]uint8, error) {
|
||||
var randomBytes [2]byte
|
||||
_, err := rand.Read(randomBytes[:])
|
||||
|
||||
return randomBytes, err
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
package lorawan
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"hash"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type cmacHash struct {
|
||||
ciph cipher.Block
|
||||
k1 []byte
|
||||
k2 []byte
|
||||
data []byte
|
||||
x []byte
|
||||
}
|
||||
|
||||
const (
|
||||
Size = aes.BlockSize
|
||||
blockSize = Size
|
||||
)
|
||||
|
||||
var (
|
||||
subkeyZero []byte
|
||||
subkeyRb []byte
|
||||
)
|
||||
|
||||
func init() {
|
||||
subkeyZero = bytes.Repeat([]byte{0x00}, blockSize)
|
||||
subkeyRb = append(bytes.Repeat([]byte{0x00}, blockSize-1), 0x87)
|
||||
}
|
||||
|
||||
// New returns an AES-CMAC hash using the supplied key. The key must be 16, 24,
|
||||
// or 32 bytes long.
|
||||
func NewCmac(key []byte) (hash.Hash, error) {
|
||||
// Create a cipher.
|
||||
ciph, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Set up the hash object.
|
||||
h := &cmacHash{ciph: ciph}
|
||||
h.k1, h.k2 = generateSubkeys(ciph)
|
||||
h.Reset()
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func Xor(dst []byte, a []byte, b []byte) error {
|
||||
if len(dst) != len(a) || len(a) != len(b) {
|
||||
panic("crypto/Xor: bad length")
|
||||
}
|
||||
for i, _ := range a {
|
||||
dst[i] = a[i] ^ b[i]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *cmacHash) Reset() {
|
||||
h.data = h.data[:0]
|
||||
h.x = make([]byte, blockSize)
|
||||
}
|
||||
|
||||
func (h *cmacHash) BlockSize() int {
|
||||
return h.ciph.BlockSize()
|
||||
}
|
||||
|
||||
func (h *cmacHash) Size() int {
|
||||
return h.ciph.BlockSize()
|
||||
}
|
||||
|
||||
func (h *cmacHash) Sum(b []byte) []byte {
|
||||
dataLen := len(h.data)
|
||||
|
||||
// We should have at most one block left.
|
||||
if dataLen > blockSize {
|
||||
panic("cmacHash err1")
|
||||
}
|
||||
|
||||
// Calculate M_last.
|
||||
mLast := make([]byte, blockSize)
|
||||
if dataLen == blockSize {
|
||||
Xor(mLast, h.data, h.k1)
|
||||
} else {
|
||||
// TODO(jacobsa): Accept a destination buffer in common.PadBlock and
|
||||
// simplify this code.
|
||||
Xor(mLast, PadBlock(h.data), h.k2)
|
||||
}
|
||||
|
||||
y := make([]byte, blockSize)
|
||||
Xor(y, mLast, h.x)
|
||||
|
||||
result := make([]byte, blockSize)
|
||||
h.ciph.Encrypt(result, y)
|
||||
|
||||
b = append(b, result...)
|
||||
return b
|
||||
}
|
||||
|
||||
func (h *cmacHash) Write(p []byte) (n int, err error) {
|
||||
n = len(p)
|
||||
|
||||
// First step: consume enough data to expand h.data to a full block, if
|
||||
// possible.
|
||||
{
|
||||
toConsume := blockSize - len(h.data)
|
||||
if toConsume > len(p) {
|
||||
toConsume = len(p)
|
||||
}
|
||||
|
||||
h.data = append(h.data, p[:toConsume]...)
|
||||
p = p[toConsume:]
|
||||
}
|
||||
|
||||
// If there's no data left in p, it means h.data might not be a full block.
|
||||
// Even if it is, we're not sure it's the final block, which we must treat
|
||||
// specially. So we must stop here.
|
||||
if len(p) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// h.data is a full block and is not the last; process it.
|
||||
h.writeBlocks(h.data)
|
||||
h.data = h.data[:0]
|
||||
|
||||
// Consume any further full blocks in p that we're sure aren't the last. Note
|
||||
// that we're sure that len(p) is greater than zero here.
|
||||
blocksToProcess := (len(p) - 1) / blockSize
|
||||
bytesToProcess := blocksToProcess * blockSize
|
||||
|
||||
h.writeBlocks(p[:bytesToProcess])
|
||||
p = p[bytesToProcess:]
|
||||
|
||||
// Store the rest for later.
|
||||
h.data = append(h.data, p...)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (h *cmacHash) writeBlocks(p []byte) {
|
||||
y := make([]byte, blockSize)
|
||||
|
||||
for off := 0; off < len(p); off += blockSize {
|
||||
block := p[off : off+blockSize]
|
||||
|
||||
xorBlock(
|
||||
unsafe.Pointer(&y[0]),
|
||||
unsafe.Pointer(&h.x[0]),
|
||||
unsafe.Pointer(&block[0]))
|
||||
|
||||
h.ciph.Encrypt(h.x, y)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func xorBlock(
|
||||
dstPtr unsafe.Pointer,
|
||||
aPtr unsafe.Pointer,
|
||||
bPtr unsafe.Pointer) {
|
||||
// Check assumptions. (These are compile-time constants, so this should
|
||||
// compile out.)
|
||||
const wordSize = unsafe.Sizeof(uintptr(0))
|
||||
if blockSize != 4*wordSize {
|
||||
panic("xorBlock err1")
|
||||
}
|
||||
|
||||
// Convert.
|
||||
a := (*[4]uintptr)(aPtr)
|
||||
b := (*[4]uintptr)(bPtr)
|
||||
dst := (*[4]uintptr)(dstPtr)
|
||||
|
||||
// Compute.
|
||||
dst[0] = a[0] ^ b[0]
|
||||
dst[1] = a[1] ^ b[1]
|
||||
dst[2] = a[2] ^ b[2]
|
||||
dst[3] = a[3] ^ b[3]
|
||||
}
|
||||
|
||||
func PadBlock(block []byte) []byte {
|
||||
blockLen := len(block)
|
||||
if blockLen >= aes.BlockSize {
|
||||
panic("PadBlock input must be less than 16 bytes.")
|
||||
}
|
||||
|
||||
result := make([]byte, aes.BlockSize)
|
||||
copy(result, block)
|
||||
result[blockLen] = 0x80
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Given the supplied cipher, whose block size must be 16 bytes, return two
|
||||
// subkeys that can be used in MAC generation. See section 5.3 of NIST SP
|
||||
// 800-38B. Note that the other NIST-approved block size of 8 bytes is not
|
||||
// supported by this function.
|
||||
func generateSubkeys(ciph cipher.Block) (k1 []byte, k2 []byte) {
|
||||
if ciph.BlockSize() != blockSize {
|
||||
panic("generateSubkeys requires a cipher with a block size of 16 bytes.")
|
||||
}
|
||||
|
||||
// Step 1
|
||||
l := make([]byte, blockSize)
|
||||
ciph.Encrypt(l, subkeyZero)
|
||||
|
||||
// Step 2: Derive the first subkey.
|
||||
if Msb(l) == 0 {
|
||||
// TODO(jacobsa): Accept a destination buffer in ShiftLeft and then hoist
|
||||
// the allocation in the else branch below.
|
||||
k1 = ShiftLeft(l)
|
||||
} else {
|
||||
k1 = make([]byte, blockSize)
|
||||
Xor(k1, ShiftLeft(l), subkeyRb)
|
||||
}
|
||||
|
||||
// Step 3: Derive the second subkey.
|
||||
if Msb(k1) == 0 {
|
||||
k2 = ShiftLeft(k1)
|
||||
} else {
|
||||
k2 = make([]byte, blockSize)
|
||||
Xor(k2, ShiftLeft(k1), subkeyRb)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func ShiftLeft(b []byte) []byte {
|
||||
l := len(b)
|
||||
if l == 0 {
|
||||
panic("shiftLeft requires a non-empty buffer.")
|
||||
}
|
||||
|
||||
output := make([]byte, l)
|
||||
|
||||
overflow := byte(0)
|
||||
for i := int(l - 1); i >= 0; i-- {
|
||||
output[i] = b[i] << 1
|
||||
output[i] |= overflow
|
||||
overflow = (b[i] & 0x80) >> 7
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
// Msb returns the most significant bit of the supplied data (which must be
|
||||
// non-empty). This is the MSB(L) function of RFC 4493.
|
||||
func Msb(buf []byte) uint8 {
|
||||
if len(buf) == 0 {
|
||||
panic("msb requires non-empty buffer.")
|
||||
}
|
||||
|
||||
return buf[0] >> 7
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package lorawan
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
// genPayloadMIC computes MIC given the payload and the key
|
||||
func genPayloadMIC(payload []uint8, key [16]uint8) [4]uint8 {
|
||||
var mic [4]uint8
|
||||
hash, _ := NewCmac(key[:])
|
||||
hash.Write(payload)
|
||||
hb := hash.Sum([]byte{})
|
||||
copy(mic[:], hb[0:4])
|
||||
return mic
|
||||
}
|
||||
|
||||
func calcMessageMIC(payload []uint8, key [16]uint8, dir uint8, addr []byte, fCnt uint32, lenMessage uint8) [4]uint8 {
|
||||
var b0 []byte
|
||||
b0 = append(b0, 0x49, 0x00, 0x00, 0x00, 0x00)
|
||||
b0 = append(b0, dir)
|
||||
b0 = append(b0, addr[:]...)
|
||||
var b [4]byte
|
||||
binary.LittleEndian.PutUint32(b[:], fCnt)
|
||||
b0 = append(b0, b[:]...)
|
||||
b0 = append(b0, 0x00)
|
||||
b0 = append(b0, lenMessage)
|
||||
|
||||
var full []byte
|
||||
full = append(full, b0...)
|
||||
full = append(full, payload...)
|
||||
|
||||
var mic [4]uint8
|
||||
hash, _ := NewCmac(key[:])
|
||||
hash.Write(full)
|
||||
hb := hash.Sum([]byte{})
|
||||
copy(mic[:], hb[0:4])
|
||||
return mic
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package lorawan
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
// Otaa is used to store Over The Air Activation data of a LoRaWAN session
|
||||
type Otaa struct {
|
||||
DevEUI [8]uint8
|
||||
AppEUI [8]uint8
|
||||
AppKey [16]uint8
|
||||
devNonce [2]uint8
|
||||
appNonce [3]uint8
|
||||
NetID [3]uint8
|
||||
buf []uint8
|
||||
}
|
||||
|
||||
// Initialize DevNonce
|
||||
func (o *Otaa) Init() {
|
||||
o.buf = make([]uint8, 0)
|
||||
o.generateDevNonce()
|
||||
}
|
||||
|
||||
func (o *Otaa) generateDevNonce() {
|
||||
// TODO: handle error
|
||||
rnd, _ := GetRand16()
|
||||
o.devNonce[0] = rnd[0]
|
||||
o.devNonce[1] = rnd[1]
|
||||
}
|
||||
|
||||
func (o *Otaa) incrementDevNonce() {
|
||||
nonce := uint16(o.devNonce[1])<<8 | uint16(o.devNonce[0]) + 1
|
||||
o.devNonce[0] = uint8(nonce)
|
||||
o.devNonce[1] = uint8((nonce >> 8))
|
||||
}
|
||||
|
||||
// Set configures the Otaa AppEUI, DevEUI, AppKey for the device
|
||||
func (o *Otaa) Set(appEUI []uint8, devEUI []uint8, appKey []uint8) {
|
||||
o.SetAppEUI(appEUI)
|
||||
o.SetDevEUI(devEUI)
|
||||
o.SetAppKey(appKey)
|
||||
}
|
||||
|
||||
// SetAppEUI configures the Otaa AppEUI
|
||||
func (o *Otaa) SetAppEUI(appEUI []uint8) error {
|
||||
if len(appEUI) != 8 {
|
||||
return ErrInvalidEuiLength
|
||||
}
|
||||
|
||||
copy(o.AppEUI[:], appEUI)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Otaa) GetAppEUI() string {
|
||||
return hex.EncodeToString(o.AppEUI[:])
|
||||
}
|
||||
|
||||
// SetDevEUI configures the Otaa DevEUI
|
||||
func (o *Otaa) SetDevEUI(devEUI []uint8) error {
|
||||
if len(devEUI) != 8 {
|
||||
return ErrInvalidEuiLength
|
||||
}
|
||||
|
||||
copy(o.DevEUI[:], devEUI)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Otaa) GetDevEUI() string {
|
||||
return hex.EncodeToString(o.DevEUI[:])
|
||||
}
|
||||
|
||||
// SetAppKey configures the Otaa AppKey
|
||||
func (o *Otaa) SetAppKey(appKey []uint8) error {
|
||||
if len(appKey) != 16 {
|
||||
return ErrInvalidAppKeyLength
|
||||
}
|
||||
|
||||
copy(o.AppKey[:], appKey)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Otaa) GetAppKey() string {
|
||||
return hex.EncodeToString(o.AppKey[:])
|
||||
}
|
||||
|
||||
func (o *Otaa) GetNetID() string {
|
||||
return hex.EncodeToString(o.NetID[:])
|
||||
}
|
||||
|
||||
func (o *Otaa) SetNetID(netID []uint8) error {
|
||||
if len(netID) != 3 {
|
||||
return ErrInvalidNetIDLength
|
||||
}
|
||||
|
||||
copy(o.NetID[:], netID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateJoinRequest Generates a LoraWAN Join request
|
||||
func (o *Otaa) GenerateJoinRequest() ([]uint8, error) {
|
||||
o.incrementDevNonce()
|
||||
|
||||
// TODO: Add checks
|
||||
o.buf = o.buf[:0]
|
||||
o.buf = append(o.buf, 0x00)
|
||||
o.buf = append(o.buf, reverseBytes(o.AppEUI[:])...)
|
||||
o.buf = append(o.buf, reverseBytes(o.DevEUI[:])...)
|
||||
o.buf = append(o.buf, o.devNonce[:]...)
|
||||
mic := genPayloadMIC(o.buf, o.AppKey)
|
||||
o.buf = append(o.buf, mic[:]...)
|
||||
|
||||
return o.buf, nil
|
||||
}
|
||||
|
||||
// DecodeJoinAccept Decodes a Lora Join Accept packet
|
||||
func (o *Otaa) DecodeJoinAccept(phyPload []uint8, s *Session) error {
|
||||
if len(phyPload) < 12 {
|
||||
return ErrInvalidPacketLength
|
||||
}
|
||||
data := phyPload[1:] // Remove trailing 0x20
|
||||
|
||||
// Prepare AES Cipher
|
||||
block, err := aes.NewCipher(o.AppKey[:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buf := make([]byte, len(data))
|
||||
for k := 0; k < len(data)/aes.BlockSize; k++ {
|
||||
block.Encrypt(buf[k*aes.BlockSize:], data[k*aes.BlockSize:])
|
||||
}
|
||||
|
||||
copy(o.appNonce[:], buf[0:3])
|
||||
copy(o.NetID[:], buf[3:6])
|
||||
copy(s.DevAddr[:], buf[6:10])
|
||||
s.DLSettings = buf[10]
|
||||
s.RXDelay = buf[11]
|
||||
|
||||
if len(buf) > 16 {
|
||||
copy(s.CFList[:], buf[12:28])
|
||||
}
|
||||
rxMic := buf[len(buf)-4:]
|
||||
|
||||
dataMic := []byte{}
|
||||
dataMic = append(dataMic, phyPload[0])
|
||||
dataMic = append(dataMic, o.appNonce[:]...)
|
||||
dataMic = append(dataMic, o.NetID[:]...)
|
||||
dataMic = append(dataMic, s.DevAddr[:]...)
|
||||
dataMic = append(dataMic, s.DLSettings)
|
||||
dataMic = append(dataMic, s.RXDelay)
|
||||
dataMic = append(dataMic, s.CFList[:]...)
|
||||
computedMic := genPayloadMIC(dataMic[:], o.AppKey)
|
||||
if !bytes.Equal(computedMic[:], rxMic[:]) {
|
||||
return ErrInvalidMic
|
||||
}
|
||||
// Generate NwkSKey
|
||||
// NwkSKey = aes128_encrypt(AppKey, 0x01|AppNonce|NetID|DevNonce|pad16)
|
||||
sKey := []byte{}
|
||||
sKey = append(sKey, 0x01)
|
||||
sKey = append(sKey, o.appNonce[:]...)
|
||||
sKey = append(sKey, o.NetID[:]...)
|
||||
sKey = append(sKey, o.devNonce[:]...)
|
||||
for i := 0; i < 7; i++ {
|
||||
sKey = append(sKey, 0x00) // PAD to 16
|
||||
}
|
||||
block.Encrypt(buf, sKey)
|
||||
copy(s.NwkSKey[:], buf[0:16])
|
||||
|
||||
// Generate AppSKey
|
||||
// AppSKey = aes128_encrypt(AppKey, 0x02|AppNonce|NetID|DevNonce|pad16)
|
||||
sKey[0] = 0x02
|
||||
block.Encrypt(buf, sKey)
|
||||
copy(s.AppSKey[:], buf[0:16])
|
||||
|
||||
// Reset counters
|
||||
s.FCntDown = 0
|
||||
s.FCntUp = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package region
|
||||
|
||||
import "tinygo.org/x/drivers/lora"
|
||||
|
||||
const (
|
||||
AU915_DEFAULT_PREAMBLE_LEN = 8
|
||||
AU915_DEFAULT_TX_POWER_DBM = 20
|
||||
)
|
||||
|
||||
type RegionSettingsAU915 struct {
|
||||
joinRequestChannel *Channel
|
||||
joinAcceptChannel *Channel
|
||||
uplinkChannel *Channel
|
||||
}
|
||||
|
||||
func AU915() *RegionSettingsAU915 {
|
||||
return &RegionSettingsAU915{
|
||||
joinRequestChannel: &Channel{lora.MHz_916_8,
|
||||
lora.Bandwidth_125_0,
|
||||
lora.SpreadingFactor9,
|
||||
lora.CodingRate4_5,
|
||||
AU915_DEFAULT_PREAMBLE_LEN,
|
||||
AU915_DEFAULT_TX_POWER_DBM},
|
||||
joinAcceptChannel: &Channel{lora.MHz_923_3,
|
||||
lora.Bandwidth_500_0,
|
||||
lora.SpreadingFactor9,
|
||||
lora.CodingRate4_5,
|
||||
AU915_DEFAULT_PREAMBLE_LEN,
|
||||
AU915_DEFAULT_TX_POWER_DBM},
|
||||
uplinkChannel: &Channel{lora.MHz_916_8,
|
||||
lora.Bandwidth_125_0,
|
||||
lora.SpreadingFactor9,
|
||||
lora.CodingRate4_5,
|
||||
AU915_DEFAULT_PREAMBLE_LEN,
|
||||
AU915_DEFAULT_TX_POWER_DBM},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RegionSettingsAU915) JoinRequestChannel() *Channel {
|
||||
return r.joinRequestChannel
|
||||
}
|
||||
|
||||
func (r *RegionSettingsAU915) JoinAcceptChannel() *Channel {
|
||||
return r.joinAcceptChannel
|
||||
}
|
||||
|
||||
func (r *RegionSettingsAU915) UplinkChannel() *Channel {
|
||||
return r.uplinkChannel
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package region
|
||||
|
||||
import "tinygo.org/x/drivers/lora"
|
||||
|
||||
const (
|
||||
EU868_DEFAULT_PREAMBLE_LEN = 8
|
||||
EU868_DEFAULT_TX_POWER_DBM = 20
|
||||
)
|
||||
|
||||
type RegionSettingsEU868 struct {
|
||||
joinRequestChannel *Channel
|
||||
joinAcceptChannel *Channel
|
||||
uplinkChannel *Channel
|
||||
}
|
||||
|
||||
func EU868() *RegionSettingsEU868 {
|
||||
return &RegionSettingsEU868{
|
||||
joinRequestChannel: &Channel{lora.MHz_868_1,
|
||||
lora.Bandwidth_125_0,
|
||||
lora.SpreadingFactor9,
|
||||
lora.CodingRate4_7,
|
||||
EU868_DEFAULT_PREAMBLE_LEN,
|
||||
EU868_DEFAULT_TX_POWER_DBM},
|
||||
joinAcceptChannel: &Channel{lora.MHz_868_1,
|
||||
lora.Bandwidth_125_0,
|
||||
lora.SpreadingFactor9,
|
||||
lora.CodingRate4_7,
|
||||
EU868_DEFAULT_PREAMBLE_LEN,
|
||||
EU868_DEFAULT_TX_POWER_DBM},
|
||||
uplinkChannel: &Channel{lora.MHz_868_1,
|
||||
lora.Bandwidth_125_0,
|
||||
lora.SpreadingFactor9,
|
||||
lora.CodingRate4_7,
|
||||
EU868_DEFAULT_PREAMBLE_LEN,
|
||||
EU868_DEFAULT_TX_POWER_DBM},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RegionSettingsEU868) JoinRequestChannel() *Channel {
|
||||
return r.joinRequestChannel
|
||||
}
|
||||
|
||||
func (r *RegionSettingsEU868) JoinAcceptChannel() *Channel {
|
||||
return r.joinAcceptChannel
|
||||
}
|
||||
|
||||
func (r *RegionSettingsEU868) UplinkChannel() *Channel {
|
||||
return r.uplinkChannel
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package region
|
||||
|
||||
type Channel struct {
|
||||
Frequency uint32
|
||||
Bandwidth uint8
|
||||
SpreadingFactor uint8
|
||||
CodingRate uint8
|
||||
PreambleLength uint16
|
||||
TxPowerDBm int8
|
||||
}
|
||||
|
||||
type RegionSettings interface {
|
||||
JoinRequestChannel() *Channel
|
||||
JoinAcceptChannel() *Channel
|
||||
UplinkChannel() *Channel
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package lorawan
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"math"
|
||||
)
|
||||
|
||||
// Session is used to store session data of a LoRaWAN session
|
||||
type Session struct {
|
||||
NwkSKey [16]uint8
|
||||
AppSKey [16]uint8
|
||||
DevAddr [4]uint8
|
||||
FCntDown uint32
|
||||
FCntUp uint32
|
||||
CFList [16]uint8
|
||||
RXDelay uint8
|
||||
DLSettings uint8
|
||||
}
|
||||
|
||||
// SetDevAddr configures the Session DevAddr
|
||||
func (s *Session) SetDevAddr(devAddr []uint8) error {
|
||||
if len(devAddr) != 4 {
|
||||
return ErrInvalidDevAddrLength
|
||||
}
|
||||
|
||||
copy(s.DevAddr[:], devAddr)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDevAddr returns the Session DevAddr
|
||||
func (s *Session) GetDevAddr() string {
|
||||
return hex.EncodeToString(s.DevAddr[:])
|
||||
}
|
||||
|
||||
// SetNwkSKey configures the Session NwkSKey
|
||||
func (s *Session) SetNwkSKey(nwkSKey []uint8) error {
|
||||
if len(nwkSKey) != 16 {
|
||||
return ErrInvalidNwkSKeyLength
|
||||
}
|
||||
|
||||
copy(s.NwkSKey[:], nwkSKey)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetNwkSKey returns the Session NwkSKey
|
||||
func (s *Session) GetNwkSKey() string {
|
||||
return hex.EncodeToString(s.NwkSKey[:])
|
||||
}
|
||||
|
||||
// SetAppSKey configures the Session AppSKey
|
||||
func (s *Session) SetAppSKey(appSKey []uint8) error {
|
||||
if len(appSKey) != 16 {
|
||||
return ErrInvalidAppSKeyLength
|
||||
}
|
||||
|
||||
copy(s.AppSKey[:], appSKey)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAppSKey returns the Session AppSKey
|
||||
func (s *Session) GetAppSKey() string {
|
||||
return hex.EncodeToString(s.AppSKey[:])
|
||||
}
|
||||
|
||||
// GenMessage generates an uplink message.
|
||||
func (s *Session) GenMessage(dir uint8, payload []uint8) ([]uint8, error) {
|
||||
var buf []uint8
|
||||
buf = append(buf, 0b01000000) // FHDR Unconfirmed up
|
||||
buf = append(buf, s.DevAddr[:]...)
|
||||
|
||||
// FCtl : No ADR, No RFU, No ACK, No FPending, No FOpt
|
||||
buf = append(buf, 0x00)
|
||||
|
||||
// FCnt Up
|
||||
buf = append(buf, uint8(s.FCntUp&0xFF), uint8((s.FCntUp>>8)&0xFF))
|
||||
|
||||
// FPort=1
|
||||
buf = append(buf, 0x01)
|
||||
|
||||
fCnt := uint32(0)
|
||||
if dir == 0 {
|
||||
fCnt = s.FCntUp
|
||||
s.FCntUp++
|
||||
} else {
|
||||
fCnt = s.FCntDown
|
||||
}
|
||||
data, err := s.genFRMPayload(dir, fCnt, payload, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf = append(buf, data[:]...)
|
||||
|
||||
mic := calcMessageMIC(buf, s.NwkSKey, dir, s.DevAddr[:], fCnt, uint8(len(buf)))
|
||||
buf = append(buf, mic[:]...)
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func (s *Session) genFRMPayload(dir uint8, fCnt uint32, payload []byte, isFOpts bool) ([]byte, error) {
|
||||
k := len(payload) / aes.BlockSize
|
||||
if len(payload)%aes.BlockSize != 0 {
|
||||
k++
|
||||
}
|
||||
if k > math.MaxUint8 {
|
||||
return nil, ErrFrmPayloadTooLarge
|
||||
}
|
||||
encrypted := make([]byte, 0, k*16)
|
||||
cipher, err := aes.NewCipher(s.AppSKey[:])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var a [aes.BlockSize]byte
|
||||
a[0] = 0x01
|
||||
a[5] = dir
|
||||
copy(a[6:10], s.DevAddr[:])
|
||||
binary.LittleEndian.PutUint32(a[10:14], fCnt)
|
||||
var ss [aes.BlockSize]byte
|
||||
var b [aes.BlockSize]byte
|
||||
for i := uint8(0); i < uint8(k); i++ {
|
||||
copy(b[:], payload[i*aes.BlockSize:])
|
||||
if !isFOpts {
|
||||
a[15] = i + 1
|
||||
}
|
||||
cipher.Encrypt(ss[:], a[:])
|
||||
for j := 0; j < aes.BlockSize; j++ {
|
||||
b[j] = b[j] ^ ss[j]
|
||||
}
|
||||
encrypted = append(encrypted, b[:]...)
|
||||
}
|
||||
return encrypted[:len(payload)], nil
|
||||
}
|
||||
@@ -1,15 +1,6 @@
|
||||
package main
|
||||
package lora
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
errRadioNotFound = errors.New("radio not found")
|
||||
errRxTimeout = errors.New("radio RX timeout")
|
||||
)
|
||||
|
||||
type LoraRadio interface {
|
||||
type Radio interface {
|
||||
Reset()
|
||||
Tx(pkt []uint8, timeoutMs uint32) error
|
||||
Rx(timeoutMs uint32) ([]uint8, error)
|
||||
@@ -19,4 +10,10 @@ type LoraRadio interface {
|
||||
SetBandwidth(bw uint8)
|
||||
SetCrc(enable bool)
|
||||
SetSpreadingFactor(sf uint8)
|
||||
SetPreambleLength(plen uint16)
|
||||
SetTxPower(txpow int8)
|
||||
SetSyncWord(syncWord uint16)
|
||||
SetPublicNetwork(enable bool)
|
||||
SetHeaderType(headerType uint8)
|
||||
LoraConfig(cnf Config)
|
||||
}
|
||||
+16
-1
@@ -32,7 +32,7 @@ func (d Device) Connected() bool {
|
||||
|
||||
// Configure sets up the device for communication.
|
||||
func (d Device) Configure() error {
|
||||
return d.bus.WriteRegister(uint8(d.Address), PWR_MGMT_1, []uint8{0})
|
||||
return d.SetClockSource(CLOCK_INTERNAL)
|
||||
}
|
||||
|
||||
// ReadAcceleration reads the current acceleration from the device and returns
|
||||
@@ -78,3 +78,18 @@ func (d Device) ReadRotation() (x int32, y int32, z int32) {
|
||||
z = int32(int16((uint16(data[4])<<8)|uint16(data[5]))) * 15625 / 2048 * 1000
|
||||
return
|
||||
}
|
||||
|
||||
// SetClockSource allows the user to configure the clock source.
|
||||
func (d Device) SetClockSource(source uint8) error {
|
||||
return d.bus.WriteRegister(uint8(d.Address), PWR_MGMT_1, []uint8{source})
|
||||
}
|
||||
|
||||
// SetFullScaleGyroRange allows the user to configure the scale range for the gyroscope.
|
||||
func (d Device) SetFullScaleGyroRange(rng uint8) error {
|
||||
return d.bus.WriteRegister(uint8(d.Address), GYRO_CONFIG, []uint8{rng})
|
||||
}
|
||||
|
||||
// SetFullScaleAccelRange allows the user to configure the scale range for the accelerometer.
|
||||
func (d Device) SetFullScaleAccelRange(rng uint8) error {
|
||||
return d.bus.WriteRegister(uint8(d.Address), ACCEL_CONFIG, []uint8{rng})
|
||||
}
|
||||
|
||||
@@ -98,6 +98,29 @@ const (
|
||||
I2C_PER3_DO = 0x66
|
||||
I2C_MST_DELAY_CT = 0x67
|
||||
|
||||
// Clock settings
|
||||
CLOCK_INTERNAL = 0x00
|
||||
CLOCK_PLL_XGYRO = 0x01
|
||||
CLOCK_PLL_YGYRO = 0x02
|
||||
CLOCK_PLL_ZGYRO = 0x03
|
||||
CLOCK_PLL_EXTERNAL_32_768_KZ = 0x04
|
||||
CLOCK_PLL_EXTERNAL_19_2_MHZ = 0x05
|
||||
CLOCK_RESERVED = 0x06
|
||||
CLOCK_STOP = 0x07
|
||||
|
||||
// Accelerometer settings
|
||||
AFS_RANGE_2G = 0x00
|
||||
AFS_RANGE_4G = 0x01
|
||||
AFS_RANGE_8G = 0x02
|
||||
AFS_RANGE_16G = 0x03
|
||||
|
||||
// Gyroscope settings
|
||||
FS_RANGE_250 = 0x00
|
||||
FS_RANGE_500 = 0x01
|
||||
FS_RANGE_1000 = 0x02
|
||||
FS_RANGE_2000 = 0x03
|
||||
|
||||
// other registers
|
||||
SIGNAL_PATH_RES = 0x68 // Signal path reset
|
||||
USER_CTRL = 0x6A // User control
|
||||
PWR_MGMT_1 = 0x6B // Power Management 1
|
||||
|
||||
@@ -2,6 +2,8 @@ package http
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -211,3 +213,41 @@ func (c *Client) Post(url, contentType string, body io.Reader) (resp *Response,
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
return c.Do(req)
|
||||
}
|
||||
|
||||
// PostForm issues a POST to the specified URL, with data's keys and
|
||||
// values URL-encoded as the request body.
|
||||
//
|
||||
// The Content-Type header is set to application/x-www-form-urlencoded.
|
||||
// To set other headers, use NewRequest and DefaultClient.Do.
|
||||
//
|
||||
// When err is nil, resp always contains a non-nil resp.Body.
|
||||
// Caller should close resp.Body when done reading from it.
|
||||
//
|
||||
// PostForm is a wrapper around DefaultClient.PostForm.
|
||||
//
|
||||
// See the Client.Do method documentation for details on how redirects
|
||||
// are handled.
|
||||
//
|
||||
// To make a request with a specified context.Context, use NewRequestWithContext
|
||||
// and DefaultClient.Do.
|
||||
func PostForm(url string, data url.Values) (resp *Response, err error) {
|
||||
return DefaultClient.PostForm(url, data)
|
||||
}
|
||||
|
||||
// PostForm issues a POST to the specified URL,
|
||||
// with data's keys and values URL-encoded as the request body.
|
||||
//
|
||||
// The Content-Type header is set to application/x-www-form-urlencoded.
|
||||
// To set other headers, use NewRequest and Client.Do.
|
||||
//
|
||||
// When err is nil, resp always contains a non-nil resp.Body.
|
||||
// Caller should close resp.Body when done reading from it.
|
||||
//
|
||||
// See the Client.Do method documentation for details on how redirects
|
||||
// are handled.
|
||||
//
|
||||
// To make a request with a specified context.Context, use NewRequestWithContext
|
||||
// and Client.Do.
|
||||
func (c *Client) PostForm(url string, data url.Values) (resp *Response, err error) {
|
||||
return c.Post(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
|
||||
}
|
||||
|
||||
+4
-1
@@ -357,7 +357,10 @@ func ParseIP(s string) IP {
|
||||
|
||||
// String returns the string form of the IP address ip.
|
||||
func (ip IP) String() string {
|
||||
return string(ip)
|
||||
if len(ip) < 4 {
|
||||
return ""
|
||||
}
|
||||
return strconv.Itoa(int(ip[0])) + "." + strconv.Itoa(int(ip[1])) + "." + strconv.Itoa(int(ip[2])) + "." + strconv.Itoa(int(ip[3]))
|
||||
}
|
||||
|
||||
// Conn is a generic stream-oriented network connection.
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package net
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestIPAddressString(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
ipaddr := ParseIP("127.0.0.1")
|
||||
|
||||
c.Assert(ipaddr.String(), qt.Equals, "127.0.0.1")
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
# SX126x LoRa Radio
|
||||
|
||||
The Semtech SX126x family of sub-Ghz radio transceivers come in a number of different formats.
|
||||
|
||||
SX126x radios have a wide continuous frequency coverage from 150 MHz to 960 MHz.
|
||||
|
||||
SX1261 can transmit up to +15 dBm and the SX1262 can transmit up to +22 dBm
|
||||
|
||||
Some development boards are specific to a particular frequency range, so for example you need a different variation of that board for EU868 vs. for AU915.
|
||||
|
||||
## LAMBDA62 RF module
|
||||
|
||||
Cost effective radio module featuring the Semtech SX1262.
|
||||
|
||||
## STM32 STM32WLE5JC
|
||||
|
||||
ST Micro STM32WLE5/E4xx is 32-bit ARM Cortex M4 processor with Semtech SX126x processor on a single die.
|
||||
|
||||
https://www.st.com/en/microcontrollers-microprocessors/stm32wle5jc.html
|
||||
|
||||
Several boards have been made using this processor.
|
||||
|
||||
### Wio-E5 mini
|
||||
|
||||
https://www.seeedstudio.com/LoRa-E5-mini-STM32WLE5JC-p-4869.html
|
||||
|
||||
Wio-E5 mini is a compacted-sized dev board suitable for the rapid testing and building of small-size LoRa device and application prototyping. Wio-E5 mini is embedded with and leads out full GPIOs of Wio-E5 STM32WLE5JC
|
||||
@@ -0,0 +1,13 @@
|
||||
package sx126x
|
||||
|
||||
// SX126X radio transceiver has several pins that control
|
||||
// RF_IN, RF_OUT, NSS, and BUSY.
|
||||
// This interface allows the creation of struct
|
||||
// that can drive the RF Switch (Used in Lora RX and Lora Tx)
|
||||
type RadioController interface {
|
||||
Init() error
|
||||
SetRfSwitchMode(mode int) error
|
||||
SetNss(state bool) error
|
||||
WaitWhileBusy() error
|
||||
SetupInterrupts(handler func()) error
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
//go:build gnse
|
||||
|
||||
/*
|
||||
Generic Node Sensor Edition
|
||||
RFSwitch
|
||||
|
||||
Disable Switch : PB8=OFF PA0=OFF PA1=OFF
|
||||
Enable RX : PB8=ON PA0=ON PA1=OFF
|
||||
Enable TX RFO LP : PB8=ON PA0=ON PA1=ON
|
||||
Enable TX RFO HP : PB8=ON PA0=OFF PA1=ON
|
||||
*/
|
||||
package sx126x
|
||||
|
||||
import (
|
||||
"machine"
|
||||
)
|
||||
|
||||
// RadioControl for GNSE board.
|
||||
type RadioControl struct {
|
||||
STM32RadioControl
|
||||
}
|
||||
|
||||
func NewRadioControl() *RadioControl {
|
||||
return &RadioControl{STM32RadioControl{}}
|
||||
}
|
||||
|
||||
// Init pins needed for controlling rx/tx
|
||||
func (rc *RadioControl) Init() error {
|
||||
machine.PA0.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
machine.PA1.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
machine.PB8.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rc *RadioControl) SetRfSwitchMode(mode int) error {
|
||||
switch mode {
|
||||
|
||||
case RFSWITCH_TX_HP:
|
||||
machine.PA0.Set(false)
|
||||
machine.PA1.Set(true)
|
||||
machine.PB8.Set(true)
|
||||
|
||||
case RFSWITCH_TX_LP:
|
||||
machine.PA0.Set(true)
|
||||
machine.PA1.Set(true)
|
||||
machine.PB8.Set(true)
|
||||
|
||||
case RFSWITCH_RX:
|
||||
machine.PA0.Set(true)
|
||||
machine.PA1.Set(false)
|
||||
machine.PB8.Set(true)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
//go:build lorae5
|
||||
|
||||
/*
|
||||
LoRa-E5 module ONLY transmits through RFO_HP:
|
||||
|
||||
Receive: PA4=1, PA5=0
|
||||
Transmit(high output power, SMPS mode): PA4=0, PA5=1
|
||||
*/
|
||||
|
||||
package sx126x
|
||||
|
||||
import (
|
||||
"machine"
|
||||
)
|
||||
|
||||
// RadioControl for LoRa-E5 board.
|
||||
type RadioControl struct {
|
||||
STM32RadioControl
|
||||
}
|
||||
|
||||
func NewRadioControl() *RadioControl {
|
||||
return &RadioControl{STM32RadioControl: STM32RadioControl{}}
|
||||
}
|
||||
|
||||
// Init pins needed for controlling rx/tx
|
||||
func (rc *RadioControl) Init() error {
|
||||
machine.PA4.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
machine.PB5.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rc *RadioControl) SetRfSwitchMode(mode int) error {
|
||||
switch mode {
|
||||
|
||||
case RFSWITCH_RX:
|
||||
machine.PA4.Set(true)
|
||||
machine.PB5.Set(false)
|
||||
case RFSWITCH_TX_LP:
|
||||
return errLowPowerTxNotSupported
|
||||
case RFSWITCH_TX_HP:
|
||||
machine.PA4.Set(false)
|
||||
machine.PB5.Set(true)
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
//go:build !stm32wlx
|
||||
|
||||
package sx126x
|
||||
|
||||
import (
|
||||
"machine"
|
||||
|
||||
"time"
|
||||
)
|
||||
|
||||
// RadioControl for boards that are connected using normal pins.
|
||||
type RadioControl struct {
|
||||
nssPin, busyPin, dio1Pin machine.Pin
|
||||
rxPin, txLowPin, txHighPin machine.Pin
|
||||
}
|
||||
|
||||
func NewRadioControl(nssPin, busyPin, dio1Pin,
|
||||
rxPin, txLowPin, txHighPin machine.Pin) *RadioControl {
|
||||
return &RadioControl{
|
||||
nssPin: nssPin,
|
||||
busyPin: busyPin,
|
||||
dio1Pin: dio1Pin,
|
||||
rxPin: rxPin,
|
||||
txLowPin: txLowPin,
|
||||
txHighPin: txHighPin,
|
||||
}
|
||||
}
|
||||
|
||||
// SetNss sets the NSS line aka chip select for SPI.
|
||||
func (rc *RadioControl) SetNss(state bool) error {
|
||||
rc.nssPin.Set(state)
|
||||
return nil
|
||||
}
|
||||
|
||||
// WaitWhileBusy wait until the radio is no longer busy
|
||||
func (rc *RadioControl) WaitWhileBusy() error {
|
||||
count := 100
|
||||
for count > 0 {
|
||||
if !rc.busyPin.Get() {
|
||||
return nil
|
||||
}
|
||||
count--
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
return errWaitWhileBusyTimeout
|
||||
}
|
||||
|
||||
// Init() configures whatever needed for sx126x radio control
|
||||
func (rc *RadioControl) Init() error {
|
||||
rc.nssPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
rc.busyPin.Configure(machine.PinConfig{Mode: machine.PinInputPulldown})
|
||||
return nil
|
||||
}
|
||||
|
||||
// add interrupt handler for Radio IRQs for pins
|
||||
func (rc *RadioControl) SetupInterrupts(handler func()) error {
|
||||
irqHandler = handler
|
||||
|
||||
rc.dio1Pin.Configure(machine.PinConfig{Mode: machine.PinInputPulldown})
|
||||
if err := rc.dio1Pin.SetInterrupt(machine.PinRising, handleInterrupt); err != nil {
|
||||
return errRadioNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var irqHandler func()
|
||||
|
||||
func handleInterrupt(machine.Pin) {
|
||||
irqHandler()
|
||||
}
|
||||
|
||||
func (rc *RadioControl) SetRfSwitchMode(mode int) error {
|
||||
switch mode {
|
||||
|
||||
case RFSWITCH_RX:
|
||||
rc.rxPin.Set(true)
|
||||
rc.txLowPin.Set(false)
|
||||
rc.txHighPin.Set(false)
|
||||
case RFSWITCH_TX_LP:
|
||||
rc.rxPin.Set(false)
|
||||
rc.txLowPin.Set(true)
|
||||
rc.txHighPin.Set(false)
|
||||
case RFSWITCH_TX_HP:
|
||||
rc.rxPin.Set(false)
|
||||
rc.txLowPin.Set(false)
|
||||
rc.txHighPin.Set(true)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -4,40 +4,28 @@ package sx126x
|
||||
|
||||
import (
|
||||
"device/stm32"
|
||||
"errors"
|
||||
"machine"
|
||||
"tinygo.org/x/drivers"
|
||||
"tinygo.org/x/drivers/lora"
|
||||
|
||||
"runtime/interrupt"
|
||||
)
|
||||
|
||||
// New creates a new SX126x connection.
|
||||
func New(spi drivers.SPI) *Device {
|
||||
c := make(chan lora.RadioEvent, 10)
|
||||
d := Device{
|
||||
spi: spi,
|
||||
radioEventChan: c,
|
||||
}
|
||||
if d.spi == machine.SPI3 {
|
||||
d.SubGhzInit()
|
||||
d.SetDeviceType(DEVICE_TYPE_SX1262)
|
||||
} else {
|
||||
panic("Driver only support SUBGHZSPI (SPI3) on stm32wlx targets")
|
||||
}
|
||||
return &d
|
||||
// STM32RadioControl helps implement the RadioController interface
|
||||
type STM32RadioControl struct {
|
||||
irqHandler func()
|
||||
}
|
||||
|
||||
// SpiSetNss Sets the NSS line
|
||||
func (d *Device) SpiSetNss(state bool) {
|
||||
// SetNss sets the NSS line aka chip select for SPI.
|
||||
func (rc *STM32RadioControl) SetNss(state bool) error {
|
||||
if state {
|
||||
stm32.PWR.SUBGHZSPICR.SetBits(stm32.PWR_SUBGHZSPICR_NSS)
|
||||
} else {
|
||||
stm32.PWR.SUBGHZSPICR.ClearBits(stm32.PWR_SUBGHZSPICR_NSS)
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WaitBusy sleep until all busy flags clears
|
||||
func (d *Device) WaitBusy() error {
|
||||
// WaitWhileBusy wait until the radio is no longer busy
|
||||
func (rc *STM32RadioControl) WaitWhileBusy() error {
|
||||
count := 100
|
||||
var rfbusyms, rfbusys bool
|
||||
for count > 0 {
|
||||
@@ -49,12 +37,11 @@ func (d *Device) WaitBusy() error {
|
||||
}
|
||||
count--
|
||||
}
|
||||
return errors.New("WaitBusy Timeout")
|
||||
return errWaitWhileBusyTimeout
|
||||
}
|
||||
|
||||
// SubGhzInit() configures internal SX1262's SPI bus.
|
||||
func (d *Device) SubGhzInit() {
|
||||
|
||||
// init() configures whatever needed for sx126x radio control
|
||||
func init() {
|
||||
// Enable APB3 Periph clock and delay
|
||||
stm32.RCC.APB3ENR.SetBits(stm32.RCC_APB3ENR_SUBGHZSPIEN)
|
||||
_ = stm32.RCC.APB3ENR.Get()
|
||||
@@ -81,5 +68,18 @@ func (d *Device) SubGhzInit() {
|
||||
stm32.SPI3.CR1.Set(stm32.SPI_CR1_MSTR | stm32.SPI_CR1_SSI | (0b010 << 3) | stm32.SPI_CR1_SSM)
|
||||
stm32.SPI3.CR2.Set(stm32.SPI_CR2_FRXTH | (0b111 << 8))
|
||||
stm32.SPI3.CR1.SetBits(stm32.SPI_CR1_SPE)
|
||||
|
||||
}
|
||||
|
||||
func (rc *STM32RadioControl) SetupInterrupts(handler func()) error {
|
||||
irqHandler = handler
|
||||
intr := interrupt.New(stm32.IRQ_Radio_IRQ_Busy, handleInterrupt)
|
||||
intr.Enable()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var irqHandler func()
|
||||
|
||||
func handleInterrupt(interrupt.Interrupt) {
|
||||
irqHandler()
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
//go:build nucleowl55jc
|
||||
|
||||
/*
|
||||
Nucleo WL55JC1
|
||||
RFSwitch
|
||||
Nucleo WL55JC1
|
||||
RFSwitch
|
||||
|
||||
+-----------+---------+------------+------------+
|
||||
| | FE_CTRL1 | FE_CTRL2 | FE_CTRL3 |
|
||||
@@ -13,42 +13,48 @@
|
||||
| RX | HIGH | LOW | HIGH |
|
||||
+-----------+----------+-----------+------------+
|
||||
*/
|
||||
package main
|
||||
package sx126x
|
||||
|
||||
import (
|
||||
"machine"
|
||||
|
||||
"tinygo.org/x/drivers/sx126x"
|
||||
)
|
||||
|
||||
type CustomSwitch struct {
|
||||
// RadioControl for Nucleo WL55JC1 board.
|
||||
type RadioControl struct {
|
||||
STM32RadioControl
|
||||
}
|
||||
|
||||
func (s CustomSwitch) InitRFSwitch() {
|
||||
func NewRadioControl() *RadioControl {
|
||||
return &RadioControl{STM32RadioControl{}}
|
||||
}
|
||||
|
||||
// Init pins needed for controlling rx/tx
|
||||
func (rc *RadioControl) Init() error {
|
||||
machine.PC4.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
machine.PC5.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
machine.PC3.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s CustomSwitch) SetRfSwitchMode(mode int) error {
|
||||
func (rc *RadioControl) SetRfSwitchMode(mode int) error {
|
||||
switch mode {
|
||||
|
||||
case sx126x.RFSWITCH_TX_HP:
|
||||
case RFSWITCH_TX_HP:
|
||||
machine.PC4.Set(false)
|
||||
machine.PC5.Set(true)
|
||||
machine.PC3.Set(true)
|
||||
|
||||
case sx126x.RFSWITCH_TX_LP:
|
||||
case RFSWITCH_TX_LP:
|
||||
machine.PC4.Set(true)
|
||||
machine.PC5.Set(true)
|
||||
machine.PC3.Set(true)
|
||||
|
||||
case sx126x.RFSWITCH_RX:
|
||||
case RFSWITCH_RX:
|
||||
machine.PC4.Set(true)
|
||||
machine.PC5.Set(false)
|
||||
machine.PC3.Set(true)
|
||||
|
||||
}
|
||||
return nil
|
||||
|
||||
return nil
|
||||
}
|
||||
+74
-42
@@ -13,13 +13,13 @@ import (
|
||||
"tinygo.org/x/drivers/lora"
|
||||
)
|
||||
|
||||
// SX126X radio transceiver RF_IN and RF_OUT may be connected
|
||||
// to RF Switch. This interface allows the creation of struct
|
||||
// that can drive the RF Switch (Used in Lora RX and Lora Tx)
|
||||
type RFSwitch interface {
|
||||
InitRFSwitch()
|
||||
SetRfSwitchMode(mode int) error
|
||||
}
|
||||
var (
|
||||
errWaitWhileBusyTimeout = errors.New("WaitWhileBusy Timeout")
|
||||
errLowPowerTxNotSupported = errors.New("RFSWITCH_TX_LP not supported")
|
||||
errRadioNotFound = errors.New("LoRa radio not found")
|
||||
errUnexpectedRxRadioEvent = errors.New("Unexpected Radio Event during RX")
|
||||
errUnexpectedTxRadioEvent = errors.New("Unexpected Radio Event during TX")
|
||||
)
|
||||
|
||||
const (
|
||||
DEVICE_TYPE_SX1261 = iota
|
||||
@@ -38,18 +38,26 @@ const (
|
||||
SPI_BUFFER_SIZE = 256
|
||||
)
|
||||
|
||||
// Device wraps an SPI connection to a SX127x device.
|
||||
// Device wraps an SPI connection to a SX126x device.
|
||||
type Device struct {
|
||||
spi drivers.SPI // SPI bus for module communication
|
||||
rstPin, csPin machine.Pin // GPIOs for reset and chip select
|
||||
rstPin machine.Pin // GPIO for reset pin
|
||||
radioEventChan chan lora.RadioEvent // Channel for Receiving events
|
||||
loraConf lora.Config // Current Lora configuration
|
||||
rfswitch RFSwitch // RF Switch, if any
|
||||
controller RadioController // to manage interactions with the radio
|
||||
deepSleep bool // Internal Sleep state
|
||||
deviceType int // sx1261,sx1262,sx1268 (defaults sx1261)
|
||||
spiBuffer [SPI_BUFFER_SIZE]uint8
|
||||
}
|
||||
|
||||
// New creates a new SX126x connection.
|
||||
func New(spi drivers.SPI) *Device {
|
||||
return &Device{
|
||||
spi: spi,
|
||||
radioEventChan: make(chan lora.RadioEvent, 10),
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
SX126X_RTC_FREQ_IN_HZ uint32 = 64000
|
||||
)
|
||||
@@ -79,10 +87,15 @@ func (d *Device) SetDeviceType(devType int) {
|
||||
d.deviceType = devType
|
||||
}
|
||||
|
||||
// SetRfSwitch let you define a custom RF Switch driver if needed
|
||||
func (d *Device) SetRfSwitch(rfswitch RFSwitch) {
|
||||
d.rfswitch = rfswitch
|
||||
d.rfswitch.InitRFSwitch()
|
||||
// SetRadioControl let you define the RadioController
|
||||
func (d *Device) SetRadioController(rc RadioController) error {
|
||||
d.controller = rc
|
||||
if err := d.controller.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
d.controller.SetupInterrupts(d.HandleInterrupt)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
@@ -126,8 +139,8 @@ func (d *Device) SetFs() {
|
||||
|
||||
// SetTxContinuousWave set device in test mode to generate a continuous wave (RF tone)
|
||||
func (d *Device) SetTxContinuousWave() {
|
||||
if d.rfswitch != nil {
|
||||
d.rfswitch.SetRfSwitchMode(RFSWITCH_TX_HP)
|
||||
if d.controller != nil {
|
||||
d.controller.SetRfSwitchMode(RFSWITCH_TX_HP)
|
||||
}
|
||||
d.ExecSetCommand(SX126X_CMD_SET_TX_CONTINUOUS_WAVE, []uint8{})
|
||||
}
|
||||
@@ -136,8 +149,8 @@ func (d *Device) SetTxContinuousWave() {
|
||||
// Take care to initialize all Lora settings like it's done in Tx before calling this function
|
||||
// If you don't init properly all the settings, it'll fail
|
||||
func (d *Device) SetTxContinuousPreamble() {
|
||||
if d.rfswitch != nil {
|
||||
d.rfswitch.SetRfSwitchMode(RFSWITCH_TX_HP)
|
||||
if d.controller != nil {
|
||||
d.controller.SetRfSwitchMode(RFSWITCH_TX_HP)
|
||||
}
|
||||
d.ExecSetCommand(SX126X_CMD_SET_TX_INFINITE_PREAMBLE, []uint8{})
|
||||
}
|
||||
@@ -235,25 +248,25 @@ func (d *Device) SetRxTxFallbackMode(fallbackMode uint8) {
|
||||
// ReadRegister reads register value
|
||||
func (d *Device) ReadRegister(addr, size uint16) ([]uint8, error) {
|
||||
d.CheckDeviceReady()
|
||||
d.SpiSetNss(false)
|
||||
d.controller.SetNss(false)
|
||||
// Send command
|
||||
cmd := []uint8{SX126X_CMD_READ_REGISTER, uint8((addr & 0xFF00) >> 8), uint8(addr & 0x00FF), 0x00}
|
||||
d.spi.Tx(cmd, nil)
|
||||
ret := d.spiBuffer[0:size]
|
||||
d.spi.Tx(nil, ret)
|
||||
d.SpiSetNss(true)
|
||||
d.WaitBusy()
|
||||
d.controller.SetNss(true)
|
||||
d.controller.WaitWhileBusy()
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// WriteRegister writes value to register
|
||||
func (d *Device) WriteRegister(addr uint16, data []uint8) {
|
||||
d.CheckDeviceReady()
|
||||
d.SpiSetNss(false)
|
||||
d.controller.SetNss(false)
|
||||
cmd := []uint8{SX126X_CMD_WRITE_REGISTER, uint8((addr & 0xFF00) >> 8), uint8(addr & 0x00FF)}
|
||||
d.spi.Tx(append(cmd, data...), nil)
|
||||
d.SpiSetNss(true)
|
||||
d.WaitBusy()
|
||||
d.controller.SetNss(true)
|
||||
d.controller.WaitWhileBusy()
|
||||
}
|
||||
|
||||
// WriteBuffer write data from current buffer position
|
||||
@@ -466,12 +479,12 @@ func (d *Device) SetModulationParams(spreadingFactor, bandwidth, codingRate, low
|
||||
// CheckDeviceReady sleep until all busy flags clears
|
||||
func (d *Device) CheckDeviceReady() error {
|
||||
if d.deepSleep == true {
|
||||
d.SpiSetNss(false)
|
||||
d.controller.SetNss(false)
|
||||
time.Sleep(time.Millisecond)
|
||||
d.SpiSetNss(true)
|
||||
d.controller.SetNss(true)
|
||||
d.deepSleep = false
|
||||
}
|
||||
return d.WaitBusy()
|
||||
return d.controller.WaitWhileBusy()
|
||||
}
|
||||
|
||||
// ExecSetCommand send a command to configure the peripheral
|
||||
@@ -482,24 +495,24 @@ func (d *Device) ExecSetCommand(cmd uint8, buf []uint8) {
|
||||
} else {
|
||||
d.deepSleep = false
|
||||
}
|
||||
d.SpiSetNss(false)
|
||||
d.controller.SetNss(false)
|
||||
// Send command and params
|
||||
d.spi.Tx(append([]uint8{cmd}, buf...), nil)
|
||||
d.SpiSetNss(true)
|
||||
d.controller.SetNss(true)
|
||||
if cmd != SX126X_CMD_SET_SLEEP {
|
||||
d.WaitBusy()
|
||||
d.controller.WaitWhileBusy()
|
||||
}
|
||||
}
|
||||
|
||||
// ExecGetCommand queries the peripheral the peripheral
|
||||
func (d *Device) ExecGetCommand(cmd uint8, size uint8) []uint8 {
|
||||
d.CheckDeviceReady()
|
||||
d.SpiSetNss(false)
|
||||
d.controller.SetNss(false)
|
||||
// Send the command and flush first status byte (as not used)
|
||||
d.spi.Tx([]uint8{cmd, 0x00}, nil)
|
||||
d.spi.Tx(nil, d.spiBuffer[:size])
|
||||
d.SpiSetNss(true)
|
||||
d.WaitBusy()
|
||||
d.controller.SetNss(true)
|
||||
d.controller.WaitWhileBusy()
|
||||
return d.spiBuffer[:size]
|
||||
}
|
||||
|
||||
@@ -510,7 +523,7 @@ func (d *Device) ExecGetCommand(cmd uint8, size uint8) []uint8 {
|
||||
// SetFrequency() Sets current Lora Frequency
|
||||
// NB: Change will be applied at next RX / TX
|
||||
func (d *Device) SetFrequency(freq uint32) {
|
||||
d.loraConf.Freq = d.loraConf.Freq
|
||||
d.loraConf.Freq = freq
|
||||
}
|
||||
|
||||
// SetIqMode() defines the current IQ Mode (Standard/Inverted)
|
||||
@@ -532,7 +545,7 @@ func (d *Device) SetCodingRate(cr uint8) {
|
||||
// SetBandwidth() sets current Lora Bandwidth
|
||||
// NB: Change will be applied at next RX / TX
|
||||
func (d *Device) SetBandwidth(bw uint8) {
|
||||
d.loraConf.Cr = bw
|
||||
d.loraConf.Bw = bw
|
||||
}
|
||||
|
||||
// SetCrc() sets current CRC mode (ON/OFF)
|
||||
@@ -545,12 +558,30 @@ func (d *Device) SetCrc(enable bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// SetSpreadingFactor setc surrent Lora Spreading Factor
|
||||
// SetSpreadingFactor sets current Lora Spreading Factor
|
||||
// NB: Change will be applied at next RX / TX
|
||||
func (d *Device) SetSpreadingFactor(sf uint8) {
|
||||
d.loraConf.Sf = sf
|
||||
}
|
||||
|
||||
// SetPreambleLength sets current Lora Preamble Length
|
||||
// NB: Change will be applied at next RX / TX
|
||||
func (d *Device) SetPreambleLength(pl uint16) {
|
||||
d.loraConf.Preamble = pl
|
||||
}
|
||||
|
||||
// SetTxPowerDbm sets current Lora TX Power in DBm
|
||||
// NB: Change will be applied at next RX / TX
|
||||
func (d *Device) SetTxPower(txpow int8) {
|
||||
d.loraConf.LoraTxPowerDBm = txpow
|
||||
}
|
||||
|
||||
// SetHeaderType sets implicit or explicit header mode
|
||||
// NB: Change will be applied at next RX / TX
|
||||
func (d *Device) SetHeaderType(headerType uint8) {
|
||||
d.loraConf.HeaderType = headerType
|
||||
}
|
||||
|
||||
//
|
||||
// Lora functions
|
||||
//
|
||||
@@ -582,8 +613,8 @@ func (d *Device) Tx(pkt []uint8, timeoutMs uint32) error {
|
||||
return lora.ErrUndefinedLoraConf
|
||||
}
|
||||
|
||||
if d.rfswitch != nil {
|
||||
err := d.rfswitch.SetRfSwitchMode(RFSWITCH_TX_HP)
|
||||
if d.controller != nil {
|
||||
err := d.controller.SetRfSwitchMode(RFSWITCH_TX_HP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -605,7 +636,7 @@ func (d *Device) Tx(pkt []uint8, timeoutMs uint32) error {
|
||||
|
||||
msg := <-d.GetRadioEventChan()
|
||||
if msg.EventType != lora.RadioEventTxDone {
|
||||
return errors.New("Unexpected Radio Event while TX")
|
||||
return errUnexpectedTxRadioEvent
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -616,8 +647,8 @@ func (d *Device) Rx(timeoutMs uint32) ([]uint8, error) {
|
||||
return nil, lora.ErrUndefinedLoraConf
|
||||
}
|
||||
|
||||
if d.rfswitch != nil {
|
||||
err := d.rfswitch.SetRfSwitchMode(RFSWITCH_RX)
|
||||
if d.controller != nil {
|
||||
err := d.controller.SetRfSwitchMode(RFSWITCH_RX)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -627,6 +658,7 @@ func (d *Device) Rx(timeoutMs uint32) ([]uint8, error) {
|
||||
irqVal := uint16(SX126X_IRQ_RX_DONE | SX126X_IRQ_TIMEOUT | SX126X_IRQ_CRC_ERR)
|
||||
d.SetStandby()
|
||||
d.SetBufferBaseAddress(0, 0)
|
||||
d.SetRfFrequency(d.loraConf.Freq)
|
||||
d.SetModulationParams(d.loraConf.Sf, bandwidth(d.loraConf.Bw), d.loraConf.Cr, d.loraConf.Ldr)
|
||||
d.SetPacketParam(d.loraConf.Preamble, d.loraConf.HeaderType, d.loraConf.Crc, 0xFF, d.loraConf.Iq)
|
||||
d.SetDioIrqParams(irqVal, irqVal, SX126X_IRQ_NONE, SX126X_IRQ_NONE)
|
||||
@@ -637,7 +669,7 @@ func (d *Device) Rx(timeoutMs uint32) ([]uint8, error) {
|
||||
if msg.EventType == lora.RadioEventTimeout {
|
||||
return nil, nil
|
||||
} else if msg.EventType != lora.RadioEventRxDone {
|
||||
return nil, errors.New("Unexpected Radio Event while RX")
|
||||
return nil, errUnexpectedRxRadioEvent
|
||||
}
|
||||
|
||||
pLen, pStart := d.GetRxBufferStatus()
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# SX127x LoRa Radio
|
||||
|
||||
This processor comes in several different packages.
|
||||
|
||||
## Adafruit LoRa Radio Featherwing
|
||||
|
||||
https://www.adafruit.com/product/3231
|
||||
|
||||
This is the LoRa 9x @ 900 MHz radio version, which can be used for either 868MHz or 915MHz transmission/reception - the exact radio frequency is determined when you load the software since it can be tuned around dynamically. They can easily go 2 Km line of sight using simple wire antennas, or up to 20Km with directional antennas and settings tweaks.
|
||||
|
||||
### Connecting
|
||||
|
||||
To use the LoRa Featherwing with a Pybadge/Gobadge, you need to connect some pads on the board itself. Solder some jumper wires as follows:
|
||||
|
||||
RST -> A
|
||||
CS -> B
|
||||
DIO1 -> C
|
||||
IRQ -> D
|
||||
|
||||
You also need to connect an antenna. The easiest is to cut a small piece of wire to the correct length based on the desired frequency:
|
||||
|
||||
EU868 Mhz - 34.54 cm
|
||||
|
||||
You can also use a fancier solution such as a uFL SMA connector with matching antenna.
|
||||
@@ -0,0 +1,10 @@
|
||||
package sx127x
|
||||
|
||||
// SX127X radio transceiver has several pins that control NSS,
|
||||
// and that are signalled when RX or TX operations are completed.
|
||||
// This interface allows the creation of types that can control this
|
||||
type RadioController interface {
|
||||
Init() error
|
||||
SetNss(state bool) error
|
||||
SetupInterrupts(handler func()) error
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package sx127x
|
||||
|
||||
import (
|
||||
"machine"
|
||||
)
|
||||
|
||||
// RadioControl for boards that are connected using normal pins.
|
||||
type RadioControl struct {
|
||||
nssPin, dio0Pin, dio1Pin machine.Pin
|
||||
}
|
||||
|
||||
func NewRadioControl(nssPin, dio0Pin, dio1Pin machine.Pin) *RadioControl {
|
||||
return &RadioControl{
|
||||
nssPin: nssPin,
|
||||
dio0Pin: dio0Pin,
|
||||
dio1Pin: dio1Pin,
|
||||
}
|
||||
}
|
||||
|
||||
// SetNss sets the NSS line aka chip select for SPI.
|
||||
func (rc *RadioControl) SetNss(state bool) error {
|
||||
rc.nssPin.Set(state)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init() configures whatever needed for sx127x radio control
|
||||
func (rc *RadioControl) Init() error {
|
||||
rc.nssPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
rc.dio0Pin.Configure(machine.PinConfig{Mode: machine.PinInputPulldown})
|
||||
rc.dio1Pin.Configure(machine.PinConfig{Mode: machine.PinInputPulldown})
|
||||
return nil
|
||||
}
|
||||
|
||||
// add interrupt handlers for Radio IRQs for pins
|
||||
func (rc *RadioControl) SetupInterrupts(handler func()) error {
|
||||
irqHandler = handler
|
||||
|
||||
// Setup DIO0 interrupt Handling
|
||||
if err := rc.dio0Pin.SetInterrupt(machine.PinRising, handleInterrupt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Setup DIO1 interrupt Handling
|
||||
if err := rc.dio1Pin.SetInterrupt(machine.PinRising, handleInterrupt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var irqHandler func()
|
||||
|
||||
func handleInterrupt(machine.Pin) {
|
||||
irqHandler()
|
||||
}
|
||||
+67
-33
@@ -21,9 +21,10 @@ const (
|
||||
// Device wraps an SPI connection to a SX127x device.
|
||||
type Device struct {
|
||||
spi drivers.SPI // SPI bus for module communication
|
||||
rstPin, csPin machine.Pin // GPIOs for reset and chip select
|
||||
rstPin machine.Pin // GPIO for reset
|
||||
radioEventChan chan lora.RadioEvent // Channel for Receiving events
|
||||
loraConf lora.Config // Current Lora configuration
|
||||
controller RadioController // to manage interactions with the radio
|
||||
deepSleep bool // Internal Sleep state
|
||||
deviceType int // sx1261,sx1262,sx1268 (defaults sx1261)
|
||||
spiBuffer [SPI_BUFFER_SIZE]uint8
|
||||
@@ -41,16 +42,26 @@ func (d *Device) GetRadioEventChan() chan lora.RadioEvent {
|
||||
}
|
||||
|
||||
// New creates a new SX127x connection. The SPI bus must already be configured.
|
||||
func New(spi machine.SPI, csPin machine.Pin, rstPin machine.Pin) *Device {
|
||||
func New(spi machine.SPI, rstPin machine.Pin) *Device {
|
||||
k := Device{
|
||||
spi: spi,
|
||||
csPin: csPin,
|
||||
rstPin: rstPin,
|
||||
radioEventChan: make(chan lora.RadioEvent, 10),
|
||||
}
|
||||
return &k
|
||||
}
|
||||
|
||||
// SetRadioControl let you define the RadioController
|
||||
func (d *Device) SetRadioController(rc RadioController) error {
|
||||
d.controller = rc
|
||||
if err := d.controller.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
d.controller.SetupInterrupts(d.HandleInterrupt)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reset re-initialize the sx127x device
|
||||
func (d *Device) Reset() {
|
||||
d.rstPin.Low()
|
||||
@@ -67,21 +78,21 @@ func (d *Device) DetectDevice() bool {
|
||||
|
||||
// ReadRegister reads register value
|
||||
func (d *Device) ReadRegister(reg uint8) uint8 {
|
||||
d.csPin.Low()
|
||||
d.controller.SetNss(false)
|
||||
d.spi.Tx([]byte{reg & 0x7f}, nil)
|
||||
var value [1]byte
|
||||
d.spi.Tx(nil, value[:])
|
||||
d.csPin.High()
|
||||
d.controller.SetNss(true)
|
||||
return value[0]
|
||||
}
|
||||
|
||||
// WriteRegister writes value to register
|
||||
func (d *Device) WriteRegister(reg uint8, value uint8) uint8 {
|
||||
var response [1]byte
|
||||
d.csPin.Low()
|
||||
d.controller.SetNss(false)
|
||||
d.spi.Tx([]byte{reg | 0x80}, nil)
|
||||
d.spi.Tx([]byte{value}, response[:])
|
||||
d.csPin.High()
|
||||
d.controller.SetNss(true)
|
||||
return response[0]
|
||||
}
|
||||
|
||||
@@ -134,8 +145,8 @@ func (d *Device) GetBandwidth() int32 {
|
||||
}
|
||||
*/
|
||||
|
||||
// SetTxPower sets the transmitter output power
|
||||
func (d *Device) SetTxPower(txPower int8, paBoost bool) {
|
||||
// SetTxPowerWithPaBoost sets the transmitter output power and may activate paBoost
|
||||
func (d *Device) SetTxPowerWithPaBoost(txPower int8, paBoost bool) {
|
||||
if !paBoost {
|
||||
// RFO
|
||||
if txPower < 0 {
|
||||
@@ -260,8 +271,8 @@ func (d *Device) SetCodingRate(cr uint8) {
|
||||
d.WriteRegister(SX127X_REG_MODEM_CONFIG_1, (d.ReadRegister(SX127X_REG_MODEM_CONFIG_1)&0xf1)|(cr<<1))
|
||||
}
|
||||
|
||||
// SetImplicitHeaderModeOn Enables implicit header mode ***
|
||||
func (d *Device) SetHeaderMode(headerType uint8) {
|
||||
// SetHeaderType set implicit or explicit mode
|
||||
func (d *Device) SetHeaderType(headerType uint8) {
|
||||
d.loraConf.HeaderType = headerType
|
||||
if headerType == lora.HeaderImplicit {
|
||||
d.WriteRegister(SX127X_REG_MODEM_CONFIG_1, d.ReadRegister(SX127X_REG_MODEM_CONFIG_1)|0x01)
|
||||
@@ -284,13 +295,10 @@ func (d *Device) SetSpreadingFactor(sf uint8) {
|
||||
d.WriteRegister(SX127X_REG_MODEM_CONFIG_2, newValue)
|
||||
}
|
||||
|
||||
// SetTxContinuousMode enable Continuous Tx mode
|
||||
func (d *Device) SetTxContinuousMode(val bool) {
|
||||
if val {
|
||||
d.WriteRegister(SX127X_REG_MODEM_CONFIG_2, d.ReadRegister(SX127X_REG_MODEM_CONFIG_2)|0x08)
|
||||
} else {
|
||||
d.WriteRegister(SX127X_REG_MODEM_CONFIG_2, d.ReadRegister(SX127X_REG_MODEM_CONFIG_2)&0xf7)
|
||||
}
|
||||
// SetTxPower sets the transmitter output (with paBoost ON)
|
||||
func (d *Device) SetTxPower(txPower int8) {
|
||||
d.loraConf.LoraTxPowerDBm = txPower
|
||||
d.SetTxPowerWithPaBoost(txPower, true)
|
||||
}
|
||||
|
||||
// SetCrc Enable CRC generation and check on payload
|
||||
@@ -304,8 +312,9 @@ func (d *Device) SetCrc(enable bool) {
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Device) SetPreamble(pLen uint16) {
|
||||
// Sets preamble length
|
||||
// SetPreambleLength defines number of preamble
|
||||
func (d *Device) SetPreambleLength(pLen uint16) {
|
||||
d.loraConf.Preamble = pLen
|
||||
d.WriteRegister(SX127X_REG_PREAMBLE_MSB, uint8((pLen>>8)&0xFF))
|
||||
d.WriteRegister(SX127X_REG_PREAMBLE_LSB, uint8(pLen&0xFF))
|
||||
}
|
||||
@@ -331,6 +340,15 @@ func (d *Device) SetIqMode(val uint8) {
|
||||
}
|
||||
}
|
||||
|
||||
// SetPublicNetwork changes Sync Word to match network type
|
||||
func (d *Device) SetPublicNetwork(enabled bool) {
|
||||
if enabled {
|
||||
d.SetSyncWord(SX127X_LORA_MAC_PUBLIC_SYNCWORD)
|
||||
} else {
|
||||
d.SetSyncWord(SX127X_LORA_MAC_PRIVATE_SYNCWORD)
|
||||
}
|
||||
}
|
||||
|
||||
// Tx sends a lora packet, (with timeout)
|
||||
func (d *Device) Tx(pkt []uint8, timeoutMs uint32) error {
|
||||
d.SetOpModeLora()
|
||||
@@ -342,15 +360,15 @@ func (d *Device) Tx(pkt []uint8, timeoutMs uint32) error {
|
||||
d.WriteRegister(SX127X_REG_LNA, SX127X_LNA_MAX_GAIN) // Set Low Noise Amplifier to MAX
|
||||
|
||||
d.SetFrequency(d.loraConf.Freq)
|
||||
d.SetPreamble(d.loraConf.Preamble)
|
||||
d.SetPreambleLength(d.loraConf.Preamble)
|
||||
d.SetSyncWord(d.loraConf.SyncWord)
|
||||
d.SetBandwidth(d.loraConf.Bw)
|
||||
d.SetSpreadingFactor(d.loraConf.Sf)
|
||||
d.SetIqMode(d.loraConf.Iq)
|
||||
d.SetCodingRate(d.loraConf.Cr)
|
||||
d.SetCrc(d.loraConf.Crc == lora.CRCOn)
|
||||
d.SetTxPower(d.loraConf.LoraTxPowerDBm, true)
|
||||
d.SetHeaderMode(d.loraConf.HeaderType)
|
||||
d.SetTxPower(d.loraConf.LoraTxPowerDBm)
|
||||
d.SetHeaderType(d.loraConf.HeaderType)
|
||||
d.SetAgcAuto(SX127X_AGC_AUTO_ON)
|
||||
|
||||
// set the IRQ mapping DIO0=TxDone DIO1=NOP DIO2=NOP
|
||||
@@ -398,15 +416,15 @@ func (d *Device) Rx(timeoutMs uint32) ([]uint8, error) {
|
||||
d.WriteRegister(SX127X_REG_LNA, SX127X_LNA_MAX_GAIN) // Set Low Noise Amplifier to MAX
|
||||
|
||||
d.SetFrequency(d.loraConf.Freq)
|
||||
d.SetPreamble(d.loraConf.Preamble)
|
||||
d.SetPreambleLength(d.loraConf.Preamble)
|
||||
d.SetSyncWord(d.loraConf.SyncWord)
|
||||
d.SetBandwidth(d.loraConf.Bw)
|
||||
d.SetSpreadingFactor(d.loraConf.Sf)
|
||||
d.SetIqMode(d.loraConf.Iq)
|
||||
d.SetCodingRate(d.loraConf.Cr)
|
||||
d.SetCrc(d.loraConf.Crc == lora.CRCOn)
|
||||
d.SetTxPower(d.loraConf.LoraTxPowerDBm, true)
|
||||
d.SetHeaderMode(d.loraConf.HeaderType)
|
||||
d.SetTxPower(d.loraConf.LoraTxPowerDBm)
|
||||
d.SetHeaderType(d.loraConf.HeaderType)
|
||||
d.SetAgcAuto(SX127X_AGC_AUTO_ON)
|
||||
|
||||
// set the IRQ mapping DIO0=RxDone DIO1=RxTimeout DIO2=NOP
|
||||
@@ -415,19 +433,26 @@ func (d *Device) Rx(timeoutMs uint32) ([]uint8, error) {
|
||||
d.WriteRegister(SX127X_REG_IRQ_FLAGS, 0xFF)
|
||||
// Mask all but RxDone
|
||||
d.WriteRegister(SX127X_REG_IRQ_FLAGS_MASK, ^(SX127X_IRQ_LORA_RXDONE_MASK | SX127X_IRQ_LORA_RXTOUT_MASK))
|
||||
// Switch to RX Mode
|
||||
d.SetOpMode(SX127X_OPMODE_RX_SINGLE) //
|
||||
// Wait for Radio Event
|
||||
|
||||
// Get Radio Event Channel
|
||||
radioCh := d.GetRadioEventChan()
|
||||
|
||||
msg := <-radioCh
|
||||
if msg.EventType == lora.RadioEventTimeout {
|
||||
// Single RX mode don't properly handle Timeouts on sx127x, so we use Continuous RX
|
||||
// Go routine is a workaround to stop the Continuous RX and fire a timeout Event
|
||||
d.SetOpMode(SX127X_OPMODE_RX)
|
||||
|
||||
var msg lora.RadioEvent
|
||||
select {
|
||||
case msg = <-radioCh:
|
||||
if msg.EventType != lora.RadioEventRxDone {
|
||||
return nil, errors.New("Unexpected Radio Event while RX " + string(0x30+msg.EventType))
|
||||
}
|
||||
case <-time.After(time.Millisecond * time.Duration(timeoutMs)):
|
||||
d.SetOpMode(SX127X_OPMODE_STANDBY)
|
||||
return nil, nil
|
||||
} else if msg.EventType != lora.RadioEventRxDone {
|
||||
return nil, errors.New("Unexpected Radio Event while RX " + string(0x30+msg.EventType))
|
||||
}
|
||||
|
||||
// Get the received payload
|
||||
d.WriteRegister(SX127X_REG_FIFO_RX_BASE_ADDR, 0)
|
||||
d.WriteRegister(SX127X_REG_FIFO_ADDR_PTR, 0)
|
||||
|
||||
@@ -440,6 +465,15 @@ func (d *Device) Rx(timeoutMs uint32) ([]uint8, error) {
|
||||
return d.spiBuffer[:pLen], nil
|
||||
}
|
||||
|
||||
// SetTxContinuousMode enable Continuous Tx mode
|
||||
func (d *Device) SetTxContinuousMode(val bool) {
|
||||
if val {
|
||||
d.WriteRegister(SX127X_REG_MODEM_CONFIG_2, d.ReadRegister(SX127X_REG_MODEM_CONFIG_2)|0x08)
|
||||
} else {
|
||||
d.WriteRegister(SX127X_REG_MODEM_CONFIG_2, d.ReadRegister(SX127X_REG_MODEM_CONFIG_2)&0xf7)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// HELPER FUNCTIONS
|
||||
//
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@ package drivers
|
||||
|
||||
// Version returns a user-readable string showing the version of the drivers package for support purposes.
|
||||
// Update this value before release of new version of software.
|
||||
const Version = "0.23.0"
|
||||
const Version = "0.24.0"
|
||||
|
||||
Reference in New Issue
Block a user