diff --git a/examples/lora/atcmd/README.md b/examples/lora/atcmd/README.md new file mode 100644 index 0000000..fda176c --- /dev/null +++ b/examples/lora/atcmd/README.md @@ -0,0 +1,29 @@ +# 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/ +``` + diff --git a/examples/lora/atcmd/commands.go b/examples/lora/atcmd/commands.go new file mode 100644 index 0000000..1126fdb --- /dev/null +++ b/examples/lora/atcmd/commands.go @@ -0,0 +1,410 @@ +package main + +import ( + "encoding/hex" + "strings" +) + +// Use to test if connection to module is OK. +func quicktest() { + writeCommandOutput("AT", "OK") +} + +// Check firmware version. +func version() { + writeCommandOutput("VER", currentVersion()+" ("+firmwareVersion()+")") +} + +// Use to check the ID of the LoRaWAN module, or change the ID. +func id(args string) error { + cmd := "ID" + + // look for comma in args + param, val, hasComma := strings.Cut(args, ",") + if hasComma { + // set + switch param { + case "DevAddr": + writeCommandOutput(cmd, "DevAddr, "+val) + case "DevEui": + writeCommandOutput(cmd, "DevEui, "+val) + case "AppEui": + writeCommandOutput(cmd, "AppEui, "+val) + default: + return errInvalidCommand + } + + return nil + } + + // get + switch param { + case "DevAddr": + writeCommandOutput(cmd, "DevAddr, xx:xx:xx:xx") + case "DevEui": + writeCommandOutput(cmd, "DevEui, xx:xx:xx:xx:xx:xx:xx:xx") + case "AppEui": + writeCommandOutput(cmd, "AppEui, xx:xx:xx:xx:xx:xx:xx:xx") + 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") + } + + return nil +} + +// Use to reset the module. If module returns error, then reset function is invalid. +func reset() error { + radio.Reset() + + writeCommandOutput("RESET", "OK") + return nil +} + +// Use to send string format frame which is no need to be confirmed by the server. +func msg(data string) error { + cmd := "MSG" + writeCommandOutput(cmd, "Start") + + if err := radio.Tx([]byte(data), defaultTimeout); err != nil { + writeCommandOutput(cmd, err.Error()) + + return err + } + + writeCommandOutput(cmd, "Done") + return nil +} + +// Use to send string format frame which must be confirmed by the server +func cmsg(data string) error { + cmd := "CMSG" + writeCommandOutput(cmd, "Start") + + if err := radio.Tx([]byte(data), defaultTimeout); err != nil { + writeCommandOutput(cmd, err.Error()) + + return err + } + + // TODO: confirmation + + writeCommandOutput(cmd, "Done") + return nil +} + +// Use to send hex format frame which is no need to be confirmed by the server +func msghex(data string) error { + cmd := "MSGHEX" + writeCommandOutput(cmd, "Start") + + writeCommandOutput(cmd, "Done") + return nil +} + +// Use to send hex format frame which must be confirmed by the server. +func cmsghex(data string) error { + cmd := "CMSGHEX" + writeCommandOutput(cmd, "Start") + + writeCommandOutput(cmd, "Done") + return nil +} + +// Use to send string format LoRaWAN proprietary frames +func pmsg(data string) error { + cmd := "PMSG" + writeCommandOutput(cmd, "Start") + + writeCommandOutput(cmd, "Done") + return nil +} + +// Use to send hex format LoRaWAN proprietary frames. +func pmsghex(data string) error { + cmd := "PMSGHEX" + writeCommandOutput(cmd, "Start") + + writeCommandOutput(cmd, "Done") + + return nil +} + +// Set PORT number which will be used by MSG/CMSG/MSGHEX/CMSGHEX command to send +// message, port number should range from 1 to 255. User should refer to LoRaWAN +// specification to choose port. +func port(p string) error { + cmd := "PMSG" + writeCommandOutput(cmd, p) + + return nil +} + +// Set ADR function of LoRaWAN module +func adr(state string) error { + cmd := "ADR" + writeCommandOutput(cmd, state) + + return nil +} + +// Use LoRaWAN defined DRx to set datarate of LoRaWAN AT modem. +func dr(rate string) error { + cmd := "DR" + writeCommandOutput(cmd, rate) + + return nil +} + +// Channel Configuration +func ch(channel string) error { + cmd := "CH" + writeCommandOutput(cmd, channel) + + return nil +} + +// Set and Check Power +func power(setting string) error { + cmd := "POWER" + writeCommandOutput(cmd, setting) + + return nil +} + +// Unconfirmed message repeats times. +func rept(setting string) error { + cmd := "REPT" + writeCommandOutput(cmd, setting) + + return nil +} + +// Confirmed message retry times. Valid range 0~254, +// if retry times is less than 2, only one message will +// be sent. Random delay 3 - 10s between each retry +// (band duty cycle limitation has the priority) +func retry(setting string) error { + cmd := "RETRY" + writeCommandOutput(cmd, setting) + + return nil +} + +func rxwin2(setting string) error { + cmd := "RXWIN2" + writeCommandOutput(cmd, setting) + + return nil +} + +func rxwin1(setting string) error { + cmd := "RXWIN1" + writeCommandOutput(cmd, setting) + + return nil +} + +func key(setting string) error { + cmd := "KEY" + writeCommandOutput(cmd, setting) + + return nil +} + +func fdefault(setting string) error { + cmd := "FDEFAULT" + writeCommandOutput(cmd, "OK") + + return nil +} + +func mode(setting string) error { + cmd := "MODE" + writeCommandOutput(cmd, setting) + + return nil +} + +func join(setting string) error { + cmd := "JOIN" + writeCommandOutput(cmd, "Starting") + + writeCommandOutput(cmd, "Done") + return nil +} + +func beacon(setting string) error { + cmd := "BEACON" + writeCommandOutput(cmd, "Starting") + + writeCommandOutput(cmd, "Done") + + return nil +} + +func class(setting string) error { + cmd := "CLASS" + writeCommandOutput(cmd, "Starting") + + writeCommandOutput(cmd, "Done") + + return nil +} + +func delay(setting string) error { + cmd := "DELAY" + writeCommandOutput(cmd, setting) + + return nil +} + +func lw(setting string) error { + cmd := "LW" + writeCommandOutput(cmd, setting) + + return nil +} + +func wdt(setting string) error { + cmd := "WDT" + writeCommandOutput(cmd, "Not implemented") + + return nil +} + +func lowpower(setting string) error { + cmd := "LOWPOWER" + writeCommandOutput(cmd, "Not implemented") + + return nil +} + +func vdd(setting string) error { + cmd := "VDD" + writeCommandOutput(cmd, "Not implemented") + + return nil +} + +func temp(setting string) error { + cmd := "TEMP" + writeCommandOutput(cmd, "Not implemented") + + return nil +} + +func rtc(setting string) error { + cmd := "RTC" + writeCommandOutput(cmd, "Not implemented") + + return nil +} + +func eeprom(setting string) error { + cmd := "EEPROM" + writeCommandOutput(cmd, "Not implemented") + + return nil +} + +func uartcmd(setting string) error { + cmd := "UART" + writeCommandOutput(cmd, "Not implemented") + + return nil +} + +func test(setting string) error { + cmd := "TEST" + writeCommandOutput(cmd, "Not implemented") + + return nil +} + +func log(setting string) error { + cmd := "LOG" + writeCommandOutput(cmd, "Not implemented") + + return nil +} + +func send(data string) error { + cmd := "SEND" + writeCommandOutput(cmd, "Start") + + // remove leading/trailing quotes + data = strings.Trim(data, "\"'") + + if err := radio.Tx([]byte(data), defaultTimeout); err != nil { + writeCommandOutput(cmd, err.Error()) + + return err + } + + writeCommandOutput(cmd, "Done") + return nil +} + +func sendhex(data string) error { + cmd := "SENDHEX" + writeCommandOutput(cmd, "Start") + + // remove leading/trailing quotes + data = strings.Trim(data, "\"'") + + // convert data from hex formatted string + data = strings.ReplaceAll(data, " ", "") + payload, err := hex.DecodeString(data) + if err != nil { + writeCommandOutput(cmd, err.Error()) + + return err + } + + if err := radio.Tx(payload, defaultTimeout); err != nil { + writeCommandOutput(cmd, err.Error()) + + return err + } + + writeCommandOutput(cmd, "Done") + return nil +} + +func recv(setting string) error { + cmd := "RECV" + + data, err := lorarx() + if err != nil { + writeCommandOutput(cmd, "ERROR "+err.Error()) + return err + } + writeCommandOutput(cmd, string(data)) + + return nil +} + +func recvhex(setting string) error { + cmd := "RECVHEX" + + data, err := lorarx() + if err != nil { + writeCommandOutput(cmd, "ERROR "+err.Error()) + return err + } + writeCommandOutput(cmd, string(data)) + + return nil +} + +func crlf() { + uart.Write([]byte("\r\n")) +} + +func writeCommandOutput(cmd, data string) { + uart.Write([]byte("+" + cmd + ": ")) + uart.Write([]byte(data)) + crlf() +} diff --git a/examples/lora/atcmd/main.go b/examples/lora/atcmd/main.go new file mode 100644 index 0000000..2f53840 --- /dev/null +++ b/examples/lora/atcmd/main.go @@ -0,0 +1,65 @@ +// AT command set console running on the device UART to communicate with +// an attached LoRa device. +// +// Computer <-> UART <-> MCU <-> SPI <-> SX126x/SX127x +// +// Connect using default baudrate for this hardware, 8-N-1 with your terminal program. +// For details on the AT command set, see: +// https://files.seeedstudio.com/products/317990687/res/LoRa-E5%20AT%20Command%20Specification_V1.0%20.pdf +package main + +import ( + "machine" + "time" +) + +// change these to test a different UART or pins if available +var ( + uart = machine.Serial + tx = machine.UART_TX_PIN + rx = machine.UART_RX_PIN + input = make([]byte, 0, 64) + + radio LoraRadio + defaultTimeout uint32 = 1000 +) + +func main() { + uart.Configure(machine.UARTConfig{TX: tx, RX: rx}) + + var err error + radio, err = setupLora() + if err != nil { + fail(err.Error()) + } + + for { + if uart.Buffered() > 0 { + data, _ := uart.ReadByte() + + switch data { + case 13: + // return key + if err := parse(input); err != nil { + uart.Write([]byte("ERROR: ")) + uart.Write([]byte(err.Error())) + crlf() + } + input = input[:0] + default: + // just capture the character + input = append(input, data) + } + } + time.Sleep(10 * time.Millisecond) + } +} + +func fail(msg string) { + for { + uart.Write([]byte(msg)) + crlf() + + time.Sleep(time.Minute) + } +} diff --git a/examples/lora/atcmd/parser.go b/examples/lora/atcmd/parser.go new file mode 100644 index 0000000..8dc9dcf --- /dev/null +++ b/examples/lora/atcmd/parser.go @@ -0,0 +1,115 @@ +package main + +import ( + "errors" + "strings" +) + +var ( + errInvalidCommand = errors.New("Invalid command") +) + +func parse(data []byte) error { + switch { + case len(data) < 2, string(data[0:2]) != "AT": + return errInvalidCommand + case len(data) == 2: + // just the AT command by itself + quicktest() + case len(data) < 6 || data[2] != '+': + return errInvalidCommand + default: + // parse the rest of the command + cmd, args, _ := strings.Cut(string(data[3:]), "=") + return parseCommand(cmd, args) + } + + return nil +} + +func parseCommand(cmd, args string) error { + switch cmd { + case "VER": + version() + case "ID": + id(args) + case "RESET": + reset() + case "MSG": + msg(args) + case "CMSG": + cmsg(args) + case "MSGHEX": + msghex(args) + case "CMSGHEX": + cmsghex(args) + case "PMSG": + pmsg(args) + case "PMSGHEX": + pmsg(args) + case "PORT": + port(args) + case "ADR": + adr(args) + case "DR": + dr(args) + case "CH": + ch(args) + case "POWER": + power(args) + case "REPT": + rept(args) + case "RETRY": + retry(args) + case "RXWIN2": + rxwin2(args) + case "RXWIN1": + rxwin1(args) + case "KEY": + key(args) + case "FDEFAULT": + fdefault(args) + case "MODE": + mode(args) + case "JOIN": + join(args) + case "BEACON": + join(args) + case "CLASS": + class(args) + case "DELAY": + delay(args) + case "LW": + lw(args) + case "WDT": + wdt(args) + case "LOWPOWER": + lowpower(args) + case "VDD": + vdd(args) + case "TEMP": + temp(args) + case "RTC": + rtc(args) + case "EEPROM": + eeprom(args) + case "UART": + uartcmd(args) + case "TEST": + test(args) + case "LOG": + log(args) + case "RECV": + recv(args) + case "RECVHEX": + recvhex(args) + case "SEND": + send(args) + case "SENDHEX": + sendhex(args) + default: + return errInvalidCommand + } + + return nil +} diff --git a/examples/lora/atcmd/radio.go b/examples/lora/atcmd/radio.go new file mode 100644 index 0000000..340f817 --- /dev/null +++ b/examples/lora/atcmd/radio.go @@ -0,0 +1,22 @@ +package main + +import ( + "errors" +) + +var ( + errRadioNotFound = errors.New("radio not found") + errRxTimeout = errors.New("radio RX timeout") +) + +type LoraRadio interface { + Reset() + Tx(pkt []uint8, timeoutMs uint32) error + Rx(timeoutMs uint32) ([]uint8, error) + SetFrequency(freq uint32) + SetIqMode(mode uint8) + SetCodingRate(cr uint8) + SetBandwidth(bw uint8) + SetCrc(enable bool) + SetSpreadingFactor(sf uint8) +} diff --git a/examples/lora/atcmd/sim.go b/examples/lora/atcmd/sim.go new file mode 100644 index 0000000..93934d3 --- /dev/null +++ b/examples/lora/atcmd/sim.go @@ -0,0 +1,37 @@ +//go:build !featherwing && !gnse && !lorae5 && !nucleowl55jc + +package main + +// do simulator setup here +func setupLora() (LoraRadio, error) { + return &SimLoraRadio{}, nil +} + +type SimLoraRadio struct { +} + +func (sr *SimLoraRadio) Reset() { +} + +func (sr *SimLoraRadio) Tx(pkt []uint8, timeoutMs uint32) error { + return nil +} + +func (sr *SimLoraRadio) Rx(timeoutMs uint32) ([]uint8, error) { + return nil, nil +} + +func (sr *SimLoraRadio) SetFrequency(freq uint32) {} +func (sr *SimLoraRadio) SetIqMode(mode uint8) {} +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 firmwareVersion() string { + return "simulator " + currentVersion() +} + +func lorarx() ([]byte, error) { + return nil, nil +} diff --git a/examples/lora/atcmd/sx126x.go b/examples/lora/atcmd/sx126x.go new file mode 100644 index 0000000..83dcbb5 --- /dev/null +++ b/examples/lora/atcmd/sx126x.go @@ -0,0 +1,73 @@ +//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) +} diff --git a/examples/lora/atcmd/sx127x.go b/examples/lora/atcmd/sx127x.go new file mode 100644 index 0000000..dd5a7de --- /dev/null +++ b/examples/lora/atcmd/sx127x.go @@ -0,0 +1,85 @@ +//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) +} diff --git a/examples/lora/atcmd/version.go b/examples/lora/atcmd/version.go new file mode 100644 index 0000000..d6c882f --- /dev/null +++ b/examples/lora/atcmd/version.go @@ -0,0 +1,7 @@ +package main + +const VERSION = "0.0.1" + +func currentVersion() string { + return VERSION +} diff --git a/examples/lora/atcmd/wl55jc.go b/examples/lora/atcmd/wl55jc.go new file mode 100644 index 0000000..8b2738a --- /dev/null +++ b/examples/lora/atcmd/wl55jc.go @@ -0,0 +1,54 @@ +//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 main + +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 + +}