Tuesday, 31 March 2026

50 Experimenting with Python and TKinter talking to an Arduino

 We have a need to send two numbers to a remote Arduino and receive a complex text string from its GPS.

I decided to use the arrow keys on the keyboard, the right and left arrows increment and decrement one of the numbers from 0 to 99 (might change). The up and down arrow keys increment and decrement the other number. These will eventually control Rudder and sail position on a model yacht, using two servos connected to an arduino on the boat. It is connected to a RFM69HCW radio transceiver (obtained from Pimoroin.co.uk, it is sold by Adafruit in the states). A PC on the shore or support boat is wired to another arduino and that arduino also has an RFM69HCW radio transceiver. The boat arduino also has a GPS module. Future extensions will add wind sensors etc.,

To get a very basic working system I decided to use the Tkinter graphics system that is supplied with python. This allows a basic crude graphic user interface (GUI) that looks dated now. There are extensions to Tkinter and there are other graphic libraries available, but I decided to keep it simple. 

In addition there are a number of ways to put graphical objects (known as widgets) on a main window. You can specify a .pack method or a .grid method. Most books will guide you through these, or just copy code and experiment. However, I used a much simpler system that uses a .place method.

The .place method is passed an x and a y value and the widget is simply placed there. This works fine until you try to resize the main window. It is also a bit tedious as you have to plan and work out where you want everything to line, I do not consider that onerous. And I don't need resizable windows.

Before presenting code let me say that I am cherry picking bits of code and ignoring others, I use a minimum of widgets, just enough to get the job done, it could be prettier.

I only use one window, I call it master, it could have been named anything, some books call the first window root. It is possible to create a second window totally within the master window, the frame widget allows this. We may need that later. I use a pulldown widget to set the comms port. I use labels and I use the .bind method to attach a function to the keys that I want to trigger an action.

That should make the code make some sense!

Also of note is that Tkinter (and most of the other GUI systems) use a different way of working from simple code. It is event driven programming. You are used to simple code where the execution flows down the page, one statement after another, with the odd loop and jump to control the flow of control.

TKinter programs initially look a bit funny, the code moves down the page but then there is a final function call. In traditional programming terms this function is entered and NEVER leaves. You might think the code would just freeze but what actually happens is that any widget defined before that final call has a number of methods attached to it and the final call creates a list of possible events and as each event happens (such as you clicking a mouse or hitting a key) that final function calls the appropriate method. After it returns you are back in that final function again. It is running an event scheduler. You can supply your own functions and these get called is they are defined as callback functions by the appropriate widget. It will make more sense once you run the program.

A further complication is that the widgets can use variables of several types but these are derived from the normal types of variable. So, whilst we might set the text attribute of a widget to some fixed text, if we want to have a string that changes we have to (a) use the textvariable attribute, not the text one. and (b) we have to use a type known as stringvar, not string. We assign values to stringvar variables using the .set(value) method and we read values out of stringvars using the .get() method. Watch out for this as it took me a while to realise this. We can trigger a function if a stringvar changes which may be useful later (I write this blog post before the project code is complete...)

Here is a demo Python/Tkinter program; (watch out for correct indenting...)

from tkinter import *

def keypress(event): # we bind the arrow keys, rest ignored here

    """Receive a keypress and move the Servos by a specified amount"""

    if event.keysym == 'Up':

        Sail.set(Sail.get()+1)

        if Sail.get() == 100:

            Sail.set(99)

    elif event.keysym == 'Down':

        Sail.set(Sail.get()-1)

        if Sail.get() < 0:

            Sail.set(0)        

    elif event.keysym == 'Right':

        Rudder.set(Rudder.get()+1)

        if Rudder.get() == 100:

            Rudder.set(99)  

    elif event.keysym == 'Left':

        Rudder.set(Rudder.get()-1)

        if Rudder.get() < 0:

            Rudder.set(0)

    else:

        pass


##########################################################

master = Tk() # every TKinter program uses this to create the first window

master.geometry("600x200") # here we use the .geometry window to set size


# Dropdown options  

Ports = [ "COM3:","COM4:","COM5:","COM6:"

         ,"COM7:","COM8:","COM9:","COM10:"

         ,"COM11:","COM12:","COM13:","COM14:"]  

# Selected option variable  

Port = StringVar()  


# Dropdown menu  

OptionMenu(master, Port, *Ports).place(x=1,y=1)  # place puts it in the window


# setup rudder and sail controls to arrow keys

Rudder = IntVar(master,50)

Sail = IntVar(master,60)


master.bind('<Up>',keypress)

master.bind('<Down>',keypress)

master.bind('<Left>',keypress)

master.bind('<Right>',keypress)


Label(master, text = "Rudder Position = ").place(x=25,y=40)

Label(master, textvariable = Rudder).place(x=125, y=40)


Label(master, text = "Sail Position = ").place(x=225,y=40)

Label(master, textvariable = Sail).place(x=305, y=40)


# could paint graphic of Rudder and Sail positions here...

#

#



# Got Comm port and rudder and sail position, send them...

# every time they change.

# and receive GPS position into a stringvar (so Tk updates them)

#


mainloop() # infinite loop needed, stringvars & widgets update... 


49 Arduino driving RFM69HCW radio transceivers

 I recently bought a couple of radio transceivers to allow slow speed transfer of data between two Arduinos. I used the RFM69HCW $10 modules from Adafruit. These do not use the LoRa protocol but a simpler packet protocol that suited my application. Point to Point.

Here in the UK I bought them from https://shop.pimoroni.com/products/adafruit-rfm69hcw-transceiver-radio-breakout?variant=19594984071 at just under a tenner, I choose the 433MHz versions as it is easier to make antennas for 70cms and I have test equipment that allows tuning such antennas.

A declared power of 100mW and range of 500m sounded good. The data sheet says 50mA (+13 dBm) to 150mA (+20dBm) current draw for transmissions, ~30mA during active radio listening. The range figure is for line of sight and depends on the antennas. My application will have a PC linked to an Arduino on shore and a mobile model boat on water, the shore station can use a Yagi or decent antenna even if the boat only uses a simple quarter wave whip.

The tutorial on the Adafruit website shows it wired to their own metro boards, I had a couple of Seeduino Nano modules and used them. Their +5 volt output can source 200mA and can power the module directly. There are Arduinos that have limited power supply outputs from the internal 5 volt regulator so you might need to power the transceivers separately in some applications.

There are two Arduino libraries that support these modules, one from LowPowerLabs and one from RadioHead. I had a lot of bother initially when I was using the LowPowerLabs libraries but have since concluded it was probably not the fault of the library. I moved on to the RadioHead Libraries as Adafruit had a modifed version in their Github account. 

Their tutorial is at https://learn.adafruit.com/adafruit-rfm69hcw-and-rfm96-rfm95-rfm98-lora-packet-padio-breakouts and the software they recommend is at https://github.com/adafruit/RadioHead 

Be very careful! the current RadioHead library at their site is version 1.145 and the Arduino IDE keeps wanting to update your IDE to this version. RESIST THE TEMPTATION TO UPDATE THE LIBRARY when a pop-up appears in your IDE. The AdaFruit version is based on version 1.121.

The Adafruit has many more examples than the up to date version, use the Adafruit version and DO NOT UPDATE it. Or at least, I know if you use the Adafruit one the code below will work, by all means, once you have a working system, change libraries, test it and let me know. I just wanted to get a basic working system and then move on (to write a Python Tkinter program to interface to the arduinos)

The Adafruit library can be downloaded as a .ZIP file from their github account. You can easily install a Zipped library into your Arduino IDE installation by copying it to your libraries folder. The Libraries folder is in your Sketch folder. By default in your Documents folder although in my installation I have moved mine to a folder on the desktop, if you move your sketch folder the libraries folder moves with it.

Place your downloaded .zip into the libraries folder and start the Arduino IDE, select the top line menu item "Sketch"->Include Library>Add  .Zip Library. Do not use the new built in library manager icon on the left of the IDE.

I initially used the simple examples you get when you install the library but couldn't get them working, I tried several different wirings and then started to look at other examples. Some had code to pulse the reset line, some examples didn't even wire up the reset pin on the module. I suspect that on some Arduinos the i/o lines come up with a rise time that allows the module to reset itself on powerup. In any case by modifying the "feather arduino" example and adding reset code and wiring I was able to get it going. YMMV.

Here is my wiring;

As you can see the SCK,MOSI and MISO pins goto the Arduino SPI pins. The CS select pin and G0 pin (interrrupt) go to suitable digital pins and are specified when you create the RH_RF69 object from the RF69 class. The RST reset pin is just wired to a digital pin that you make an output. 

I stripped the code down to the minimum and it is listed below. 

Note the "tx" code calls .send and then .waitPacketSent before using .waitAvailableTimeout to wait for data which is picked up with a .recv call. Just for interest I pickup the Received Signal Strength Indication (RSSI) to see how well the antennas are working and range tests (yet to done). The method .lastRssi returns a number in dB (I think). Setup uses the methods .init, .setFrequency, .setTxPower and .setEncryption

Note the receive code has similar setup code and its loop section calls .available to see if data has arrived. The .recv method gets the data into a buffer and I, again, read the signal strength of the last received packet with .lastRssi. A quick .send and .waitPacketSent gives a handshake back to the transmitter code to repeat the cycle

If you look into the .zip library and read the .h and .cpp files you can see the documentation, or at least a list of the various methods (functions) available to you.

// From Feather RawDemo Tx

#include <SPI.h>
#include <RH_RF69.h>  //Version 1.121 has more examples than later, don't update for now!!!
#define RF69_FREQ 434.0
#define RFM69_CS     10  // "B"
#define RFM69_INT    2 // "C"
#define RFM69_RST    6  // "A"
uint8_t rudder_position=49;// pick up new values from PC
uint8_t sail_position=48;
RH_RF69 rf69(RFM69_CS, RFM69_INT); // create Object
void setup() {
  Serial.begin(115200);
  pinMode(RFM69_RST, OUTPUT);
  digitalWrite(RFM69_RST, LOW);delay(10);// slow reset
  digitalWrite(RFM69_RST, HIGH);delay(10);
  digitalWrite(RFM69_RST, LOW);delay(10);
  if (!rf69.init()) {Serial.println("RFM69 radio init failed");while (1);}//hang
  Serial.println("RFM69 radio init OK!");
  if (!rf69.setFrequency(RF69_FREQ)) {Serial.println("setFrequency failed");}
  rf69.setTxPower(20, true);// 2nd argument must be true for RFM69HCW
 
  // The encryption key has to be the same as the one in the server
  uint8_t key[] = { 0x04, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x02,
                    0x04, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x02};//IMcC chose..
  rf69.setEncryptionKey(key);
}
void loop() {
  delay(2000);// don't flood the receiver, could make bigger...
  char radiopacket[7] = "r00s00";// Rudder and sail position
  // Pick up variables from PC here, test with 00 <<<<<
  rudder_position=51;// test
  sail_position=61;// test
  itoa(rudder_position, radiopacket+1, 10);// overwrite first 00, base 10
  itoa(sail_position, radiopacket+4, 10);// overwrite 2nd 00, base 10
  Serial.println(radiopacket);
  // Send a message!
  rf69.send((uint8_t *)radiopacket, strlen(radiopacket));
  rf69.waitPacketSent();
  // Now wait for a reply
  uint8_t buf[RH_RF69_MAX_MESSAGE_LEN];
  uint8_t len = sizeof(buf);
  if (rf69.waitAvailableTimeout(500)) {
    // Should be a reply message for us now
    if (rf69.recv(buf, &len)) {
      Serial.println((char*)buf);
      // Should be GPS position, pass on up to PC
      Serial.print("RSSI: ");Serial.println(rf69.lastRssi(), DEC);
    } else {
      Serial.println("Receive failed");
    }
  } else {
    Serial.println("No reply");
  }
}
Here also is the code for a suitable receiver

// From Feather RawDemo RX
#include <SPI.h>
#include <RH_RF69.h> //Version 1.121 has more examples than later, don't update for now!!!

#define RF69_FREQ 434.0
#define RFM69_CS     10
#define RFM69_INT    2
#define RFM69_RST    6

uint8_t rudder_position=49;
uint8_t sail_position=48;
uint8_t buf[RH_RF69_MAX_MESSAGE_LEN];
uint8_t len = sizeof(buf);

RH_RF69 rf69(RFM69_CS, RFM69_INT); // create object

void setup() {
  Serial.begin(115200);

  pinMode(RFM69_RST, OUTPUT);
  digitalWrite(RFM69_RST, LOW);delay(10);// slow reset
  digitalWrite(RFM69_RST, HIGH);delay(10);
  digitalWrite(RFM69_RST, LOW);delay(10);

  if (!rf69.init()) {Serial.println("RFM69 radio init failed"); while (1);}// hang
  Serial.println("RFM69 radio init OK!");
  if (!rf69.setFrequency(RF69_FREQ)) {Serial.println("setFrequency failed");}
  rf69.setTxPower(20, true);// 2nd argument must be true for RFM69HCW

  // The encryption key has to be the same as the one in the server
  uint8_t key[] = { 0x04, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x02,
                    0x04, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x02};//IMcC chose..
  rf69.setEncryptionKey(key);
}

void loop() {
 if (rf69.available()) {
    // Should be a message for us now
    if (rf69.recv(buf, &len)) {
      if (!len) return;// if zero no data
      buf[len] = 0;// terminate string
      rudder_position=(buf[2]-'\0')*10+(buf[2]-'\0');
      sail_position=(buf[4]-'\0')*10+(buf[5]-'\0');
      // now have two decimal numbers 0 to 99
      // how do you print numbers in the arduino IDE?



      Serial.print("RSSI: ");Serial.println(rf69.lastRssi(), DEC);
      // call function here to get position, course & speed<<<<
      strcpy(buf, "55.1234N,005.4321W,045,12");
      rf69.send(buf, sizeof(buf));
      rf69.waitPacketSent();
      Serial.println("Sent a reply");
    } else {
      Serial.println("Receive failed");
    }
  }// fi .available
}//--end loop--//


------------ ends as of March 2026 -----------------