Compare commits

...

20 Commits

Author SHA1 Message Date
deadprogram 68d4991da1 ateccx08: inttial implementation for ATECCx08
This working implementation for the ATECCx08 family of cryptgraphic processors
has random number generation and other needed supporting functions.

It also includes a sample of how to connect it to the Go crypto/rand package.

More cryptographic functions await a future interation.

Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-01-20 20:44:51 +01:00
deadprogram f2637a87d2 gps: improve parsing and add tests to verify
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-01-17 07:48:22 +01:00
deadprogram ecb343b3b3 gps: improve error handling
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-01-17 07:48:22 +01:00
deadprogram 0b62d4fb4a gps: add support for GLL sentence type, add original sentence to gps errors
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-01-17 07:48:22 +01:00
deadprogram 5b461ec2d1 gps: improve error handling
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-01-17 07:48:22 +01:00
Vincent Janelle b1c9612158 (#501) make IP.String() method return something sensible 2023-01-16 13:43:59 +01:00
deadprogram 0b6e1d0e78 lorawan: increment devnonce don't just create a new one each join attempt
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-01-16 09:33:13 +01:00
deadprogram 49e7a6d6c8 sx126x: actually set the frequency when calling SetFrequency()
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-01-16 09:33:13 +01:00
deadprogram be644b36fe lorawan: simplify GetRand16() function
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-01-16 09:33:13 +01:00
deadprogram 409723e496 lorawan: set devNonce to new random value on each attempt to join
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-01-16 09:33:13 +01:00
deadprogram 8aa3530c8b lorawan: increase timeout values for tx/rx
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-01-16 09:33:13 +01:00
deadprogram 390854ee45 examples: set pin mappings for sx126x external board
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-01-16 09:33:13 +01:00
deadprogram ce5dd65dad sx126x: pre-define all errors to avoid heap allocations
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-01-16 09:33:13 +01:00
deadprogram 73e9d5bfec sx126x: move RadioController into separate file for clarity
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-01-16 09:33:13 +01:00
deadprogram 462d2b3315 sx126x: correct RX/TX pin mapping for TheThingsIndustries GNSE board
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-01-16 09:33:13 +01:00
deadprogram 9e70ecbe64 sx126x: there is no dio0 just dio1
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-01-16 09:33:13 +01:00
deadprogram d6d06396b4 sx126x: refactor to RadioController interface to more easily handle non-STM32WL boards and remove duplicated code
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-01-16 09:33:13 +01:00
deadprogram 80b2a4a569 lora/lorawan: set functions for keys
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-01-16 09:33:13 +01:00
deadprogram 556aa9e6d3 examples: lorawan atcmd outputs needed info after Join to reuse, aka 'OTAA provisioning'
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-01-16 09:33:13 +01:00
deadprogram 4bc8ecdbdd lora/lorawan: completed functions needed for Join OTAA process
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-01-16 09:33:13 +01:00
36 changed files with 1281 additions and 363 deletions
+2 -1
View File
@@ -52,7 +52,7 @@ func main() {
## Currently supported devices
The following 90 devices are supported.
The following 91 devices are supported.
| Device Name | Interface Type |
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------|
@@ -63,6 +63,7 @@ The following 90 devices are supported.
| [APA102 RGB LED](https://cdn-shop.adafruit.com/product-files/2343/APA102C.pdf) | SPI |
| [APDS9960 Digital proximity, ambient light, RGB and gesture sensor](https://cdn.sparkfun.com/assets/learn_tutorials/3/2/1/Avago-APDS-9960-datasheet.pdf) | I2C |
| [AT24CX 2-wire serial EEPROM](https://www.openimpulse.com/blog/wp-content/uploads/wpsc/downloadables/24C32-Datasheet.pdf) | I2C |
| [ATECCx08 cryptographic processor](https://datasheet.octopart.com/ATSAMA5D27-WLSOM1-Microchip-datasheet-149595509.pdf) | I2C |
| [AXP192 single Cell Li-Battery and Power System Management](https://github.com/m5stack/M5-Schematic/blob/master/Core/AXP192%20Datasheet_v1.1_en_draft_2211.pdf) | I2C |
| [BBC micro:bit LED matrix](https://github.com/bbcmicrobit/hardware/blob/master/SCH_BBC-Microbit_V1.3B.pdf) | GPIO |
| [BH1750 ambient light sensor](https://www.mouser.com/ds/2/348/bh1750fvi-e-186247.pdf) | I2C |
+258
View File
@@ -0,0 +1,258 @@
// Package ateccx08 provides a driver for the ATECCx08 I2C cryptographic co-processor.
//
// Datasheet: https://datasheet.octopart.com/ATSAMA5D27-WLSOM1-Microchip-datasheet-149595509.pdf
package ateccx08 // import "tinygo.org/x/drivers/ateccx08"
import (
"errors"
"time"
"tinygo.org/x/drivers"
)
var (
maxCommandTime = (200 + 50) * time.Millisecond
)
var (
ErrWakeup = errors.New("error on wakeup")
ErrInvalidCRCCheck = errors.New("invalid CRC check")
ErrLockFailed = errors.New("locked failed")
)
type Device struct {
bus drivers.I2C
Address uint8
}
// New returns ATECCx08 device for the provided I2C bus using default address.
func New(i2c drivers.I2C) *Device {
return &Device{
bus: i2c,
Address: Address,
}
}
// Configure the ATECCx08 device.
func (d *Device) Configure() error {
return nil
}
// Connected returns whether ATECCx08 has been found.
func (d *Device) Connected() bool {
if err := d.Wakeup(); err != nil {
return false
}
v, err := d.Version()
if err != nil {
return false
}
return (v == ATECC508 || v == ATECC608)
}
// Wakeup the ATECC by trying to write something to address 0x00
func (d *Device) Wakeup() error {
d.bus.Tx(uint16(0x0), []byte{0x00}, nil)
time.Sleep(1500 * time.Microsecond)
d.bus.Tx(uint16(d.Address), []byte{0x00}, nil)
time.Sleep(maxCommandTime)
var status [4]byte
if err := d.readResponse(status[:]); err != nil {
return err
}
if status[0] != StatusAfterWake {
return ErrWakeup
}
return nil
}
// Sleep puts the ATECC to sleep.
func (d *Device) Sleep() {
d.bus.Tx(uint16(d.Address), []byte{0x01}, nil)
time.Sleep(time.Millisecond)
}
// Idle puts the ATECC in idle mode.
func (d *Device) Idle() {
d.bus.Tx(uint16(d.Address), []byte{0x02}, nil)
time.Sleep(time.Millisecond)
}
type ATECCVersion uint16
func (at ATECCVersion) String() string {
switch at {
case ATECC508:
return "ATECC508"
case ATECC608:
return "ATECC608"
case ATECCNone:
return "No ATECCx08"
default:
return "Unknown"
}
}
// Version returns what version of ATECC is being used.
// Either ATECC508, ATECC608, or ATECCNone.
func (d *Device) Version() (ATECCVersion, error) {
var version [4]byte
d.Wakeup()
defer d.Idle()
d.sendCommand(cmdInfo, 0x00, 0, nil)
time.Sleep(maxCommandTime)
if err := d.readResponse(version[:]); err != nil {
return ATECCNone, err
}
return ATECCVersion(uint16(version[2])<<8 | uint16(version[3])&0xf000), nil
}
// Random returns an array of 32 byte-sized random numbers.
func (d *Device) Random() ([32]byte, error) {
var random [32]byte
d.Wakeup()
defer d.Idle()
d.sendCommand(cmdRandom, 0x00, 0, nil)
time.Sleep(23 * time.Millisecond)
err := d.readResponse(random[:])
return random, err
}
// Read reads from the device memory.
func (d *Device) Read(zone, address int, data []byte) error {
d.Wakeup()
defer d.Idle()
d.sendCommand(cmdRead, byte(zone), uint16(address), nil)
time.Sleep(5 * time.Millisecond)
return d.readResponse(data)
}
// IsLocked checks to see if the ATECC is locked.
// Config zone (0) must be locked to generate random numbers.
func (d *Device) IsLocked() bool {
return d.IsZoneLocked(0)
}
// IsZoneLocked checks to see if a specific zone in the ATECC is locked.
func (d *Device) IsZoneLocked(zone int) bool {
var config [4]byte
if zone < 0 || zone > 8 {
return false
}
switch zone {
case 0, 1:
if err := d.Read(0, 0x15, config[:]); err != nil {
return false
}
// LockConfig
loc := 3
// LockData
if zone == 1 {
loc = 2
}
if config[loc] == 0 {
return true
}
default:
if err := d.Read(0, 0x16, config[:]); err != nil {
return false
}
slot := byte(zone<<2) | 2
if (config[0] & slot) == 0 {
return true
}
return false
}
return false
}
// Lock locks a zone in the device.
// Note that you cannot unlock a device zone once locked,
// so make sure you know what you are doing!
func (d *Device) Lock(zone int) error {
var status [1]byte
d.Wakeup()
defer d.Idle()
d.sendCommand(cmdLock, byte(zone)|0x80, 0, nil)
time.Sleep(32 * time.Millisecond)
d.readResponse(status[:])
if status[0] != 0 {
return ErrLockFailed
}
return nil
}
var cmdBuf [64]byte
func (d *Device) sendCommand(opcode, param1 byte, param2 uint16, data []byte) error {
cmdBuf[0] = 0x03
cmdBuf[1] = byte(8 + len(data) - 1)
cmdBuf[2] = opcode
cmdBuf[3] = param1
cmdBuf[4] = byte(param2 & 0xff)
cmdBuf[5] = byte(param2 >> 8)
copy(cmdBuf[6:], data)
crc := crc16(cmdBuf[1 : 6+len(data)])
cmdBuf[6+len(data)] = crc[0]
cmdBuf[6+len(data)+1] = crc[1]
if err := d.bus.Tx(uint16(d.Address), cmdBuf[:6+len(data)+2], nil); err != nil {
return err
}
time.Sleep(time.Millisecond)
return nil
}
func (d *Device) readResponse(data []byte) error {
var sz [1]byte
if err := d.bus.Tx(uint16(d.Address), []byte{cmdAddress}, sz[:]); err != nil {
return err
}
rx := make([]byte, sz[0])
if err := d.bus.Tx(uint16(d.Address), []byte{cmdAddress}, rx); err != nil {
return err
}
size := len(rx) - 2
payload := rx[:size]
payloaddata := rx[1:size]
payloadcrc := rx[size:]
crcCheck := crc16(payload)
if !(crcCheck[0] == payloadcrc[0] &&
crcCheck[1] == payloadcrc[1]) {
return ErrInvalidCRCCheck
}
copy(data, payloaddata)
return nil
}
+32
View File
@@ -0,0 +1,32 @@
// from https://github.com/usbarmory/armoryctl/blob/master/atecc608/atecc608.go#L104
// thank you!
package ateccx08
const (
CRC16Poly uint16 = 0x8005
)
func crc16(data []byte) []byte {
var crc uint16
for i := 0; i < len(data); i++ {
for shift := uint8(0x01); shift > 0x00; shift <<= 1 {
// data and crc bits
var d uint8
var c uint8
if uint8(data[i])&uint8(shift) != 0 {
d = 1
}
c = uint8(crc >> 15)
crc <<= 1
if d != c {
crc ^= CRC16Poly
}
}
}
return []byte{byte(crc & 0xff), byte(crc >> 8)}
}
+39
View File
@@ -0,0 +1,39 @@
package ateccx08
const (
// Address is default I2C address.
Address = 0x60
)
const (
ATECCNone = 0
ATECC508 = 0x5000
ATECC608 = 0x6000
)
const (
cmdAddress = 0x03
cmdCounter = 0x24
cmdGenKey = 0x40
cmdInfo = 0x30
cmdLock = 0x17
cmdNonce = 0x16
cmdRandom = 0x1B
cmdSHA = 0x47
cmdSign = 0x41
cmdWrite = 0x12
cmdRead = 0x02
)
const (
StatusSuccess = 0x00
StatusMiscompare = 0x01
StatusParseError = 0x03
StatusECCFault = 0x05
StatusSelfTestError = 0x07
StatusHealthTestError = 0x08
StatusExecutionError = 0x0f
StatusAfterWake = 0x11
StatusWatchdogExpire = 0xee
StatusCRCError = 0xff
)
+53
View File
@@ -0,0 +1,53 @@
package main
import (
"machine"
"encoding/hex"
"time"
"tinygo.org/x/drivers/ateccx08"
)
func main() {
time.Sleep(5 * time.Second)
println("Looking for ATECCx08...")
machine.I2C0.Configure(machine.I2CConfig{})
atecc := ateccx08.New(machine.I2C0)
atecc.Configure()
if !atecc.Connected() {
for {
println("could not connect to ATECCx08")
time.Sleep(time.Second)
}
}
version, _ := atecc.Version()
println(version.String(), "started")
if !atecc.IsLocked() {
for i := 10; i > 0; i-- {
println(version.String(), "is not locked. Locking in", i, "seconds...")
time.Sleep(time.Second)
}
// locks the Configuration zone... PERMANENTLY!
atecc.Lock(0)
}
println(version.String(), "locked.")
for {
data, err := atecc.Random()
if err != nil {
println(err)
}
println(hex.EncodeToString(data[:]))
time.Sleep(500 * time.Millisecond)
}
}
+49
View File
@@ -0,0 +1,49 @@
package main
import (
"machine"
"crypto/rand"
"encoding/hex"
"time"
"tinygo.org/x/drivers/ateccx08"
)
var atecc *ateccx08.Device
func main() {
time.Sleep(5 * time.Second)
println("Looking for ATECCx08...")
machine.I2C0.Configure(machine.I2CConfig{})
atecc = ateccx08.New(machine.I2C0)
atecc.Configure()
if !atecc.Connected() {
for {
println("could not connect to ATECCx08")
time.Sleep(time.Second)
}
}
version, _ := atecc.Version()
println(version.String(), "started")
if !atecc.IsLocked() {
for {
println(version.String(), "is not locked. Random numbers will not actually be random.")
time.Sleep(time.Second)
}
}
var result [13]byte
for {
rand.Read(result[:])
encodedString := hex.EncodeToString(result[:])
println(encodedString)
time.Sleep(500 * time.Millisecond)
}
}
+52
View File
@@ -0,0 +1,52 @@
// connects the Go crypto/rand package to the random number generation
// on the ATECCx08 cryptographic processor.
package main
import (
"crypto/rand"
"errors"
)
var (
errNoATECC = errors.New("no ATECCx08")
)
func init() {
rand.Reader = &reader{}
}
type reader struct{}
func (r *reader) Read(b []byte) (n int, err error) {
if len(b) == 0 {
return
}
if atecc == nil {
return 0, errNoATECC
}
if !atecc.IsLocked() {
panic("ATECCx08 is not locked and cannot produce random numbers!")
}
rnds, err := atecc.Random()
if err != nil {
return 0, err
}
for i := 0; i < len(b); i += 32 {
if i+32 > len(b) {
copy(b[i:], rnds[:(len(b)-i)])
break
}
copy(b[i:], rnds[:])
rnds, err = atecc.Random()
if err != nil {
return 0, err
}
}
return len(b), nil
}
+8 -1
View File
@@ -289,6 +289,8 @@ 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 {
@@ -298,7 +300,12 @@ func join(setting string) error {
}
writeCommandOutput(cmd, "Network joined")
// TODO: display NetID and DevAddr
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
}
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build !featherwing && !gnse && !lorae5 && !nucleowl55jc
//go:build !featherwing && !stm32wlx && !sx126x
package common
+65
View File
@@ -0,0 +1,65 @@
//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
}
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
}
func FirmwareVersion() string {
return "sx126x"
}
func Lorarx() ([]byte, error) {
return loraRadio.Rx(LORA_DEFAULT_RXTIMEOUT_MS)
}
+14 -18
View File
@@ -1,13 +1,9 @@
//go:build gnse || lorae5 || nucleowl55jc
//go:build !stm32wlx && sx126x
package common
import (
"device/stm32"
"machine"
"runtime/interrupt"
rfswitch "tinygo.org/x/drivers/examples/sx126x/rfswitch"
"tinygo.org/x/drivers/lora"
"tinygo.org/x/drivers/sx126x"
@@ -23,23 +19,28 @@ 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(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())
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,
@@ -59,11 +60,6 @@ func SetupLora() (lora.Radio, error) {
return loraRadio, nil
}
// radioIntHandler will take care of radio interrupts
func radioIntHandler(intr interrupt.Interrupt) {
loraRadio.HandleInterrupt()
}
func FirmwareVersion() string {
return "sx126x"
}
@@ -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 {
@@ -76,6 +73,5 @@ func main() {
loraRadio.SetStandby()
time.Sleep(60 * time.Second)
}
}
+19
View File
@@ -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()
}
+7 -19
View File
@@ -4,13 +4,9 @@ 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"
)
@@ -27,23 +23,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,10 +43,6 @@ 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,
@@ -73,10 +61,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)
+29
View File
@@ -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()
}
-60
View File
@@ -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
}
-42
View File
@@ -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
}
-54
View File
@@ -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
}
+46 -6
View File
@@ -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
View File
@@ -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 {
+97
View File
@@ -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))
}
+25 -14
View File
@@ -6,10 +6,28 @@ import (
"tinygo.org/x/drivers/lora"
)
const LORA_RXTX_TIMEOUT = 1000
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")
)
const (
LORA_TX_TIMEOUT = 2000
LORA_RX_TIMEOUT = 5000
)
var (
ActiveRadio lora.Radio
Retries = 15
)
func UseRadio(r lora.Radio) {
@@ -23,7 +41,7 @@ func Join(otaa *Otaa, session *Session) error {
var resp []uint8
if ActiveRadio == nil {
return errors.New("no LoRa radio attached")
return ErrNoRadioAttached
}
otaa.Init()
@@ -36,16 +54,15 @@ func Join(otaa *Otaa, session *Session) error {
ActiveRadio.SetCrc(true)
ActiveRadio.SetIqMode(0) // IQ Standard
ActiveRadio.Tx(payload, LORA_RXTX_TIMEOUT)
ActiveRadio.Tx(payload, LORA_TX_TIMEOUT)
if err != nil {
return err
}
// Wait for JoinAccept
//println("lorawan: Wait for JOINACCEPT for 10s")
ActiveRadio.SetIqMode(1) // IQ Inverted
for i := 0; i < 15; i++ {
resp, err = ActiveRadio.Rx(LORA_RXTX_TIMEOUT)
for i := 0; i < Retries; i++ {
resp, err = ActiveRadio.Rx(LORA_RX_TIMEOUT)
if err != nil {
return err
}
@@ -54,19 +71,13 @@ func Join(otaa *Otaa, session *Session) error {
}
}
if resp == nil {
return errors.New("no JoinAccept packet received")
return ErrNoJoinAcceptReceived
}
//println("lorawan: Received packet: len=", len(resp), "payload=", bytesToHexString(resp))
err = otaa.DecodeJoinAccept(resp, session)
if err != nil {
return err
}
// println("lorawan: Valid JOINACCEPT, now connected")
// println("lorawan: | DevAddr: ", bytesToHexString(r.Session.DevAddr[:]), " (LSB)")
// println("lorawan: | NetID : ", bytesToHexString(r.Otaa.NetID[:]))
// println("lorawan: | NwkSKey: ", bytesToHexString(r.Session.NwkSKey[:]))
// println("lorawan: | AppSKey: ", bytesToHexString(r.Session.AppSKey[:]))
return nil
}
@@ -78,7 +89,7 @@ func SendUplink(data []uint8, session *Session) error {
}
ActiveRadio.SetCrc(true)
ActiveRadio.SetIqMode(0) // IQ Standard
ActiveRadio.Tx(payload, LORA_RXTX_TIMEOUT)
ActiveRadio.Tx(payload, LORA_TX_TIMEOUT)
if err != nil {
return err
}
+3 -6
View File
@@ -13,11 +13,8 @@ func reverseBytes(s []byte) []byte {
// GetRand16 returns 2 random bytes
func GetRand16() ([2]uint8, error) {
randomBytes := make([]byte, 2)
_, err := rand.Read(randomBytes)
if err != nil {
return [2]uint8{}, err
}
var randomBytes [2]byte
_, err := rand.Read(randomBytes[:])
return [2]uint8{randomBytes[0], randomBytes[1]}, nil
return randomBytes, err
}
+41 -16
View File
@@ -4,7 +4,6 @@ import (
"bytes"
"crypto/aes"
"encoding/hex"
"errors"
)
// Otaa is used to store Over The Air Activation data of a LoRaWAN session
@@ -12,8 +11,8 @@ type Otaa struct {
DevEUI [8]uint8
AppEUI [8]uint8
AppKey [16]uint8
DevNonce [2]uint8
AppNonce [3]uint8
devNonce [2]uint8
appNonce [3]uint8
NetID [3]uint8
buf []uint8
}
@@ -22,10 +21,20 @@ type Otaa struct {
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]
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
@@ -38,7 +47,7 @@ func (o *Otaa) Set(appEUI []uint8, devEUI []uint8, appKey []uint8) {
// SetAppEUI configures the Otaa AppEUI
func (o *Otaa) SetAppEUI(appEUI []uint8) error {
if len(appEUI) != 8 {
return errors.New("invalid length")
return ErrInvalidEuiLength
}
copy(o.AppEUI[:], appEUI)
@@ -53,7 +62,7 @@ func (o *Otaa) GetAppEUI() string {
// SetDevEUI configures the Otaa DevEUI
func (o *Otaa) SetDevEUI(devEUI []uint8) error {
if len(devEUI) != 8 {
return errors.New("invalid length")
return ErrInvalidEuiLength
}
copy(o.DevEUI[:], devEUI)
@@ -68,7 +77,7 @@ func (o *Otaa) GetDevEUI() string {
// SetAppKey configures the Otaa AppKey
func (o *Otaa) SetAppKey(appKey []uint8) error {
if len(appKey) != 16 {
return errors.New("invalid length")
return ErrInvalidAppKeyLength
}
copy(o.AppKey[:], appKey)
@@ -80,14 +89,30 @@ 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[:]...)
o.buf = append(o.buf, o.devNonce[:]...)
mic := genPayloadMIC(o.buf, o.AppKey)
o.buf = append(o.buf, mic[:]...)
@@ -97,21 +122,21 @@ func (o *Otaa) GenerateJoinRequest() ([]uint8, error) {
// DecodeJoinAccept Decodes a Lora Join Accept packet
func (o *Otaa) DecodeJoinAccept(phyPload []uint8, s *Session) error {
if len(phyPload) < 12 {
return errors.New("Bad packet")
return ErrInvalidPacketLength
}
data := phyPload[1:] // Remove trailing 0x20
// Prepare AES Cipher
block, err := aes.NewCipher(o.AppKey[:])
if err != nil {
return errors.New("Lora Cipher error 1")
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.appNonce[:], buf[0:3])
copy(o.NetID[:], buf[3:6])
copy(s.DevAddr[:], buf[6:10])
s.DLSettings = buf[10]
@@ -124,7 +149,7 @@ func (o *Otaa) DecodeJoinAccept(phyPload []uint8, s *Session) error {
dataMic := []byte{}
dataMic = append(dataMic, phyPload[0])
dataMic = append(dataMic, o.AppNonce[:]...)
dataMic = append(dataMic, o.appNonce[:]...)
dataMic = append(dataMic, o.NetID[:]...)
dataMic = append(dataMic, s.DevAddr[:]...)
dataMic = append(dataMic, s.DLSettings)
@@ -132,15 +157,15 @@ func (o *Otaa) DecodeJoinAccept(phyPload []uint8, s *Session) error {
dataMic = append(dataMic, s.CFList[:]...)
computedMic := genPayloadMIC(dataMic[:], o.AppKey)
if !bytes.Equal(computedMic[:], rxMic[:]) {
return errors.New("invalid Mic")
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.appNonce[:]...)
sKey = append(sKey, o.NetID[:]...)
sKey = append(sKey, o.DevNonce[:]...)
sKey = append(sKey, o.devNonce[:]...)
for i := 0; i < 7; i++ {
sKey = append(sKey, 0x00) // PAD to 16
}
+36 -4
View File
@@ -4,7 +4,6 @@ import (
"crypto/aes"
"encoding/binary"
"encoding/hex"
"errors"
"math"
)
@@ -23,7 +22,7 @@ type Session struct {
// SetDevAddr configures the Session DevAddr
func (s *Session) SetDevAddr(devAddr []uint8) error {
if len(devAddr) != 4 {
return errors.New("invalid length")
return ErrInvalidDevAddrLength
}
copy(s.DevAddr[:], devAddr)
@@ -31,11 +30,44 @@ func (s *Session) SetDevAddr(devAddr []uint8) error {
return nil
}
// GetDevAddr returns the Session DevAddr
func (s *Session) GetDevAddr() string {
return hex.EncodeToString(s.DevAddr[:])
}
// GenMessage Forge an uplink message
// 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
@@ -75,7 +107,7 @@ func (s *Session) genFRMPayload(dir uint8, fCnt uint32, payload []byte, isFOpts
k++
}
if k > math.MaxUint8 {
return nil, errors.New("Payload too big !")
return nil, ErrFrmPayloadTooLarge
}
encrypted := make([]byte, 0, k*16)
cipher, err := aes.NewCipher(s.AppSKey[:])
+4 -1
View File
@@ -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.
+14
View File
@@ -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")
}
+13
View File
@@ -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
}
+56
View File
@@ -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
}
+47
View File
@@ -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
}
+91
View File
@@ -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 common
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
}
+53 -40
View File
@@ -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)
@@ -582,8 +595,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 +618,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 +629,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
}
@@ -637,7 +650,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()