This is a collection of Python scripts (2 done, still evolving, more to come) and other info about the RAK3272S Breakout Board. For now it comes with an AT Firmware. Since the STM32WL is a new chip, it is quite a bit complicated to set up an environment to write C++ code for it (I've has a look at STM32CubeIDE, and I'm still dizzy from the deep dive...), so for now I am focusing on writing code that make using the AT firmware a little less painful and more user-friendly.
The STM32WL has been added to the STM32 Arduino BSP, sans the LoRa part. Once LoRa is available, I'll port my Minimal LoRa firmware to this chip, and we'll be able to use something a little less kludgy. In between, I'm going to work on tools that mimic the behavior of Minimal LoRa.
There are 2 scripts that do one thing each only: receiving or sending. The way the AT firmware is set up for P2P, it is a little complicated to have both. Basically, the firmware puts the chip in Tx mode, and if you want to receive, you have to let the firmware know, giving it a timeout value (stupid idea if you ask me). It's a 16-bit value, so the max value, 65535, means wait forever. Anything else, it waits that amount of milliseconds. Pfffrt. But what's worse, even if you're in the "wait forever" mode, once you receive a line, you're back to Tx mode. THAT is unusual cruelty.
So I had two breakout boards, and each was connected to a computer running one script. At startup, the script puts the firmware in P2P mode, sets up parameters (Frequency, SF, BW, etc) and sets Tx or Rx mode. The receiver will reset to receive mode every time a message is received.
Since these 2 operate on the same network of BastWAN devices I have, I need to emulate the protocol:
- Messages are JSON packets. Not the most efficient, size-wise, but very easy to parse.
- Messages are encrypted with AES256. So Sender needs to pad the packet and encrypt it. Receiver decrypts it.
- But messages also have HMAC added to them. The last 28 bytes is the signature. Sender calculates SHA224 and tacks it on. Receiver compares the last 28 bytes with an HMAC it calculates on the rest of the packet.
- Packets all have
UUID
,from
andcmd
keys. After which, there may or may not be other keys. Sender adds nothing else, but some of my BastWAN devices do. Receiver knows what to display. It also timestamps the packets so that you can keep a log of packets.
All in all, they already do quite a bit. I am looking into merging them, and adding more Minimal_Lora options. A more robust solution could involve making a GUI app in Xojo. We'll see...
I added a new script, RAK3272S_Minimal_LoRa.py
, which mimics the BastWAN_Minimal_LoRa
firmware to an extent, and I have been having more issues with SF/BW combinations related to payloads. At SF 10, BW 7 (125 KHz), packets sent from BastWAN devices are received by the whole network, both BastWAN and RAK3272S. On the other hand, packets sent by RAK3272S are not received, AT ALL, by BastWAN devices, and garbled on other RAK3272S. Checking on my SDR, something is indeed being sent, but the BastWAN devices don't even react to it. I think either the AT firmware, or the hardware itself, is much stricter than needed when it comes to payload limitations: after all, if the RAK3272S can receive long (~180 bytes) payloads, it should be able to send them too (and they were slightly shorter, about 160 bytes).
So while I confer with the product team, I have changed the SF/BW combo to SF 10, BW 9 (500 KHz) and long packets work fine now. SF 9, BW 7 also seems to work. I am adding various JSON prefs files for testing. You can pick a prefs file at launch by doing:
python3 RAK3272S_Minimal_LoRa.py /dev/tty.usbserial-A901LHDG sf9bw7.json
I have added a few commands from Minimal_LoRa:
knownFunctions = [
["/p", sendPing, 0], ["/>", sendMsg, 1], ["/hm", setHmac, 1],
["/cr", setCr, 1], ["/tx", setTx, 1], ["/bw", setBw, 1],
["/sf", setSf, 1], ["/r", setRP, 1], ["/fq", setFq, 1],
["/as", setAs, 1], ["/e", setEnc, 1], ["/dn", setDeviceName, 1],
["/PW", setPwd, 1], ["/save", savePrefs, 0], ["/msl", sendMSL, 1],
["/gps", setGPS, 1], ["/help", showHelp, 0]
]
This is still a bit fragile, but seems to be working well enough! Have fun!
Added a calcMaxPayload()
function that calculates the maximum payload you can send based on the SF/BW configuration. Returns -1 if (supposedly anyway) invalid. Data sourced from TTN. This calculation is displayed when calling packOptions()
, ie when resetting options for the chip.
I refactored the part of the code that parses user input and evaluates commands. The commands, the relevant Fn, and whether they need an argument or not, is stored in an array, knownFunctions
. A function, testFn(line)
, parses the array, and if it finds a match, calls the relevant function. This makes it much easier to add commands.
knownFunctions = [
# Updated list as of 2021/08/05
["/p", sendPing, 0], ["/>", sendMsg, 1], ["/hm", setHmac, 1],
["/cr", setCr, 1], ["/tx", setTx, 1], ["/bw", setBw, 1],
["/sf", setSf, 1], ["/r", setRP, 1], ["/fq", setFq, 1],
["/as", setAs, 1], ["/e", setEnc, 1], ["/dn", setDeviceName, 1],
["/PW", setPwd, 1], ["/save", savePrefs, 0]
]
def testFn(line):
# This function takes one line from user input
# And looks for a know command (see above)
# If the command requires no arguments, 3rd value
# in the array is 0, and the Fn is called as is.
# Or the remainder of the line is passed as argument.
# eg:
# '/p' PING, no argument need. ["/p", sendPing, 0]
# '/fq' Set Frequency, frequency needs to be passed: ["/fq", setFq, 1]
global knownFunctions
for x in knownFunctions:
if line.startswith(x[0]):
if x[2] == 0:
x[1]()
else:
param = line[len(x[0]):]
x[1](param)
return
print("Unknown command!")
I added a couple of commands:
/e0
~/e1
Turns AES OFF or ON/PWxxxxxx
Sets password to xxxx/dnxxxxxx
Sets Device Name to xxxx/save
Saves prefs to the current prefs file
Intercepted the serial.SerialException
raised when trying to open a non-existent port. The code exists in a much cleaner way.
Ported my C++ hexDump()
function to Python to show packets in hex form for debug purposes. Called only during sendPacket()
for now, but should be added later in a few other places. Finally, it will be made optional with a #DEBUG
-like define.
The function has been updated to allow for buffers long than 256 bytes.
I added logs: main events (saving prefs, sending/receiving packets) are logged to a new log file, created at startup time from a random UUID: Log_<UUID>.log
. I will this functionality optional, but for now, while I am testing, I'll leave it on.
I have added more commands, including /help
and gps
. This is also reflected in the preferences, where the GPS position will be saved, if added manually via the /gps
command. JSON keyword addGPS
. As shown below, the /help
commands provides an explanation of the commands (the __doc__
property of each function).
knownFunctions = [
["/p", sendPing, 0], ["/>", sendMsg, 1], ["/hm", setHmac, 1],
["/cr", setCr, 1], ["/tx", setTx, 1], ["/bw", setBw, 1],
["/sf", setSf, 1], ["/r", setRP, 1], ["/fq", setFq, 1],
["/as", setAs, 1], ["/e", setEnc, 1], ["/dn", setDeviceName, 1],
["/PW", setPwd, 1], ["/save", savePrefs, 0], ["/msl", sendMSL, 1],
["/gps", setGPS, 1], ["/help", showHelp, 0]
]
Here's the output of the /help
command:
/help
/p: Sends a ping packet. 0 args
/>: Sends a custom packet (message). 1 args
/hm: Sets HMAC parameter (0/1). 1 args
/cr: Sets C/R parameter (5..8). 1 args
/tx: Sets Tx power (7..22). 1 args
/bw: Sets bandwidth parameter (7..9). 1 args
/sf: Sets spreading factor parameter (6..12). 1 args
/r: Sets pong back parameter (0/1). 1 args
/fq: Sets LoRa frequency. 1 args
/as: Sets autosend parameter (0/XX seconds). 1 args
/e: Sets AES encryption parameter (0/1). 1 args
/dn: Sets device name. 1 args
/PW: Sets AES encryption key. 1 args
/save: Saves preferences to disk. 0 args
/msl: Sets Mean Sea Level air pressure (dor altitude calculation). 1 args
/gps: Sets GPS coords (or turns off GPS location). 1 args
/help: Shows this help. 0 args