This project explores the Wi-Fi capabilities of the Nano 33 IoT to remotely control a QN8066-based FM transmitter using a Python application.
Devices and components
Arduino Nano 33 IoT
Resistance, 200 ohms
10k resistance
Materials and tools
Welding Station
Software and tools
Arduino IDE
Python 3
Project description
Nano 33 IoT Controller QN8066
1/*
2 Nano 33 IoT board
3
4 This application allows remote control of a transmitter based on the QN8066 through a
5 Python program using a Socket connection. The Arduino sketch for the NANO 33 IoT implements
6 a server that connects to a Wi-Fi network and listens on port 8066. The Python application,
7 in turn, connects to the server or service provided by the NANO 33 IoT and sends configuration
8 commands to the QN8066 transmitter.
9
10 Nano 33 IoT Wire up
11
12 | Device name | QN8066 Pin | Nano 33 IoT |
13 | --------------------------| -------------------- | ----------------- |
14 | QN8066 | | |
15 | | VCC | 3.3V |
16 | | GND | GND |
17 | | SDIO / SDA (pin 2) | A4 |
18 | | SCLK (pin 1) | A5 |
19 | --------------------------| ---------------------| ----------------- |
20
21
22 1. A suggestion if you intend to use PWM to control the RF output power of an amplifier.
23
24 Prototype documentation: https://pu2clr.github.io/QN8066/
25 PU2CLR QN8066 API documentation: https://pu2clr.github.io/QN8066/extras/apidoc/html/
26
27 By PU2CLR, Ricardo, Oct 2024.
28*/
29#include <QN8066.h>
30#include <WiFiNINA.h>
31
32#define SOCKET_PORT 8066
33
34#define RDS_PS_REFRESH_TIME 7000
35#define RDS_RT_REFRESH_TIME 17000
36#define RDS_DT_REFRESH_TIME 59000 // Date and Time Service
37
38long rdsTimePS = millis();
39long rdsTimeRT = millis();
40long rdsDateTime = millis();
41
42uint16_t currentFrequency = 1069; // 106.9 MHz
43uint16_t previousFrequency = 1069; // 106.9 MHz
44
45uint8_t currentPower = 0;
46
47//
48char ps[9] = "QN8066 \r";
49char rt[33] = "NANO33 IOT FM TX REMOTE CONTROL\r";
50
51// WI-FI Setup
52char ssid[] = "PU2CLR"; // Wi-Fi network name
53char pass[] = "pu2clr123456"; // Wi-Fi password
54
55int status = WL_IDLE_STATUS;
56WiFiServer server(SOCKET_PORT); // Create a server that listens on port 8066
57
58String receivedData = "";
59String field = "";
60String value = "";
61
62QN8066 tx;
63
64void setup() {
65 // Start serial communication
66 Serial.begin(9600);
67 delay(1000);
68
69 Serial.println("Start connecting...");
70 Serial.flush();
71
72 // Check for Wi-Fi module
73 if (WiFi.status() == WL_NO_MODULE) {
74 Serial.println("WiFi module not detected!");
75 while (true);
76 }
77
78 // Connect to Wi-Fi network
79 while (status != WL_CONNECTED) {
80 Serial.print("Attempting to connect to SSID: ");
81 Serial.println(ssid);
82 status = WiFi.begin(ssid, pass);
83 delay(10000);
84 }
85
86 // Start the server
87 server.begin();
88 Serial.println("Server started on port 8066");
89 printWiFiStatus();
90
91 if (!tx.detectDevice()) {
92 Serial.println("\nQN8066 not detected");
93 while (true);
94 }
95 Serial.println("\nQN8066 Detected");
96 tx.setup();
97 tx.setTX(currentFrequency); // Sets frequency to 106.9 MHz
98 delay(500);
99 startRDS();
100
101}
102
103// Print the Wi-Fi status (IP address, etc.)
104void printWiFiStatus() {
105 Serial.print("Connected to ");
106 Serial.println(WiFi.SSID());
107 Serial.print("IP Address: ");
108 Serial.println(WiFi.localIP());
109}
110
111
112void startRDS() {
113 tx.rdsTxEnable(true);
114 delay(200);
115 tx.rdsInitTx(0x8,0x1,0x9B, 0, 25, 6); // See: https://pu2clr.github.io/QN8066/extras/apidoc/html/index.html)
116}
117
118
119void sendRDS() {
120
121 // PS refreshing control
122 if ((millis() - rdsTimePS) > RDS_PS_REFRESH_TIME) {
123 tx.rdsSendPS(ps);
124 rdsTimePS = millis();
125 }
126
127 // RT refreshing control
128 if ((millis() - rdsTimeRT) > RDS_RT_REFRESH_TIME) {
129 tx.rdsSendRT(rt); // See rdsSendRTMessage in https://pu2clr.github.io/QN8066/extras/apidoc/html/index.html
130 rdsTimeRT = millis();
131 }
132
133 // Date Time Service refreshing control
134 /*
135 if ((millis() - rdsDateTime) > RDS_DT_REFRESH_TIME) {
136 printLocalTime();
137 struct tm timeinfo;
138 getLocalTime(&timeinfo);
139 // Sends RDS local Date and Time
140 tx.rdsSendDateTime(timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, timeinfo.tm_hour, timeinfo.tm_min);
141 rdsDateTime = millis();
142 }
143 */
144}
145
146// Sets the Nano 33 IoT Real Time Clock
147void setRTC(String datetime) {
148 // TODO
149}
150
151// Parse and execute the QN8066 command
152String processCommand(String command) {
153 int nLen;
154 int separatorIndex = command.indexOf('=');
155 String field = command.substring(0, separatorIndex); // Field name
156 String value = command.substring(separatorIndex + 1); // Field value
157
158 // Process the QN8066 command
159 if (field == "frequency") {
160 uint16_t currentFrequency = (uint16_t) ( value.toFloat() * 10.0);
161 tx.setTX(currentFrequency);
162 return "Frequency set to: " + String(currentFrequency);
163 } else if (field == "rds_pty") {
164 tx.rdsSetPTY(value.toInt());
165 tx.rdsSendPS(ps);
166 return "RDS PTY set to: " + String(value);
167 } else if (field == "rds_ps") {
168 nLen = value.length();
169 strncpy(ps, value.c_str(), nLen);
170 ps[nLen] = '\r';
171 ps[nLen+1] = '\0';
172 tx.rdsSendPS(ps);
173 return "RDS PS set to: " + value;
174 } else if (field == "rds_rt") {
175 nLen = value.length();
176 strncpy(rt, value.c_str(), nLen);
177 ps[nLen] = '\r';
178 ps[nLen+1] = '\0';
179 tx.rdsSendRT(rt);
180 return "RDS RT set to: " + value;
181 } else if (field == "stereo_mono") {
182 tx.setTxMono(value.toInt()); //
183 return "Stereo/Mono Set to: " + String(value);
184 } else if (field == "pre_emphasis") {
185 tx.setPreEmphasis(value.toInt());
186 return "Pre-Emphasis set to: " + String(value);
187 } else if (field == "impedance") {
188 tx.setTxInputImpedance(value.toInt());
189 return "Impedance set to: " + String(value);
190 } else if (field == "buffer_gain") {
191 tx.setTxInputBufferGain(value.toInt());
192 return "Impedance set to: " + String(value);
193 } else if (field == "freq_dev") {
194 float fd = value.toFloat();
195 tx.rdsSetFrequencyDerivation((uint8_t) (fd / 0.69) );
196 return "Frequency Deviation set to: " + String(fd);
197 } else if (field == "soft_clip") {
198 tx.setTxInputBufferGain(value.toInt());
199 return "Soft Clip set to: " + String(value);
200 } else if (field == "datetime") {
201 // setRTC(value);
202 return "Local Date and Time set to: " + String(value);
203 }
204 return "OK";
205}
206
207
208
209
210void loop() {
211 // Check for an incoming client
212 WiFiClient client = server.available();
213
214 if (client) {
215 Serial.println("Client connected");
216 // Read incoming data from client
217 while (client.connected()) {
218 if (client.available()) {
219 String command = client.readString();
220 String result = processCommand(command);
221 client.println(result);
222 }
223 sendRDS();
224 }
225 client.stop();
226 Serial.println("Client disconnected");
227 }
228 sendRDS();
229}
QN8066 remote control
python
1# This program uses a socket connection to communicate with the NANO 33 IoT (running the
2# QN8066_CONTROLLER.ino sketch) in order to control the QN8066-based transmitter.
3# The socket connection uses the IP provided by your WiFi network's DHCP and obtained
4# by the NANO 33 IoT. Check the IP in the Arduino IDE console (Serial Monitor).
5# The port numer used here is 8066 (QN8066_CONTROLLER.ino). You can change it if you need it.
6#
7# RDS message updates such as PTY, PS, RT, and Time are not executed immediately.
8# This may depend on the receiver's update timing as well as the distribution of
9# each message's timing from the connected controller.
10#
11# defined for the connection is 8066.
12# Author: Ricardo Lima Caratti - Sep. 2024
13
14import tkinter as tk
15from tkinter import ttk
16import socket
17from datetime import datetime
18
19# Function to send data via socket to the NANO33.
20# Change the IP below to the address indicated in the Arduino sketch linked to this application.
21def send_to_NANO33(field, value):
22 try:
23 # The IP information can be get usind the Arduino IDE (Serial Monitor)
24 NANO33_ip = '10.0.0.94' # NANO33 IP - Check it in the Arduino IDE Serial Monitor (console)
25 NANO33_port = 8066 # Defined in the ESP 32 Arduino Sketch
26 message = f"{field}={value}\n"
27
28 # Connects to the NANO33 and sends the message.
29 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
30 s.connect((NANO33_ip, NANO33_port))
31 s.sendall(message.encode())
32 response = s.recv(1024)
33 print(f'Received from NANO33 ({field}):', response.decode())
34 except socket.timeout:
35 print(f"Connection to NANO33 timed out. The device may be offline.")
36 except ConnectionRefusedError:
37 print(f"Connection to NANO33 was refused. Is the device online?")
38 except socket.error as e:
39 print(f"Socket error occurred: {e}")
40 except Exception as e:
41 print(f"An unexpected error occurred: {e}")
42
43# Specific functions for each field.
44
45def send_frequency():
46 frequency = frequency_var.get()
47 send_to_NANO33("frequency", frequency)
48
49def send_rds_pty():
50 selected_description = rds_pty_combobox.get()
51 selected_value = pty_map[selected_description]
52 print(f"Program Type (PTY): {selected_value} ({selected_description})")
53 send_to_NANO33("rds_pty", selected_value)
54
55def send_rds_ps():
56 rds_ps = rds_ps_var.get()
57 send_to_NANO33("rds_ps", rds_ps)
58
59def send_rds_rt():
60 rds_rt = rds_rt_var.get()
61 send_to_NANO33("rds_rt", rds_rt)
62
63def send_stereo_mono():
64 selected_description = stereo_mono_combobox.get()
65 selected_value = stereo_mono_map[selected_description]
66 print(f"Selected Stereo/Mono: {selected_value} ({selected_description})")
67 send_to_NANO33("stereo_mono", selected_value)
68
69def send_pre_emphasis():
70 selected_description = pre_emphasis_combobox.get()
71 selected_value = pre_emphasis_map[selected_description] # Obtém o valor numérico correspondente
72 print(f"Pre-Emphasis: {selected_value} ({selected_description})")
73 send_to_NANO33("pre_emphasis", selected_value)
74
75def send_impedance():
76 selected_description = impedance_combobox.get() # Obtém a descrição selecionada
77 selected_value = impedance_map[selected_description] # Obtém o valor numérico correspondente
78 print(f"Selected Impedance: {selected_value} ({selected_description})")
79 send_to_NANO33("impedance", selected_value)
80
81def send_buffer_gain():
82 selected_description = buffer_gain_combobox.get()
83 selected_value = buffer_gain_map[selected_description] # Obtém o valor numérico correspondente
84 print(f"Selected Buffer Gain: {selected_value} ({selected_description})")
85 send_to_NANO33("buffer_gain", selected_value)
86
87def send_freq_dev():
88 freq_dev = freq_dev_var.get()
89 send_to_NANO33("freq_dev", freq_dev)
90
91def send_soft_clip():
92 selected_description = soft_clip_combobox.get()
93 selected_value = soft_clip_map[selected_description]
94 print(f"Selected Soft CLip: {selected_value} ({selected_description})")
95 send_to_NANO33("soft_clip", selected_value)
96
97def send_datetime():
98 datetime_str = datetime_var.get()
99 send_to_NANO33("datetime", datetime_str)
100
101
102# Creating the main window with Tkinter.
103root = tk.Tk()
104root.title("NANO33 QN8066 FM Transmitter Control")
105root.configure(bg='#006400') # Green
106
107# Fields
108frequency_var = tk.StringVar(value = "106.9")
109rds_pty_var = tk.StringVar(value = "No program")
110rds_ps_var = tk.StringVar(value="PU2CLR")
111rds_rt_var = tk.StringVar(value="QN8066 Arduino Library")
112stereo_mono_var = tk.StringVar(value = "Stereo")
113pre_emphasis_var = tk.StringVar(value = "70us")
114buffer_gain_var = tk.StringVar(value = "6dB")
115impedance_var = tk.StringVar(value = "20K")
116freq_dev_var = tk.StringVar(value = "74.5")
117soft_clip_var = tk.StringVar(value = "Disable")
118datetime_var = tk.StringVar(value=datetime.now().strftime("%Y/%m/%d %H:%M") )
119
120label_fg = '#FFFF00'
121entry_bg = '#004d00'
122entry_fg = '#FFFF00'
123
124impedance_map = {
125 '10K': 0,
126 '20K': 1,
127 '40K': 2,
128 '80K': 3
129}
130
131pty_map = {'No program':0,
132 'News':1,
133 'Information':3,
134 'Sport':4,
135 'Education':5,
136 'Culture':7,
137 'Science':8,
138 'Pop Music':10,
139 'Weather':16,
140 'Religion':20,
141 'Documentary':29,
142 'Alarm':30}
143
144stereo_mono_map = {'Stereo':0,'Mono':1}
145pre_emphasis_map = {'50us':0,'75us':1}
146buffer_gain_map = {'3d{B':0,'6dB':1,'9dB':2,'12dB':3,'15dB':4,'18dB':5}
147soft_clip_map = {'Disable':0,'Enable':1}
148
149impedance_descriptions = list(impedance_map.keys())
150pty_descriptions = list(pty_map.keys())
151stereo_mono_descriptions = list(stereo_mono_map.keys())
152pre_emphasis_descriptions = list(pre_emphasis_map.keys())
153buffer_gain_descriptions = list(buffer_gain_map.keys())
154soft_clip_descriptions = list(soft_clip_map.keys())
155
156# Forms Layout
157tk.Label(root, text="Transmission Frequency (MHz):", bg='#006400', fg=label_fg).grid(row=0, column=0, sticky=tk.E, padx=10, pady=5)
158tk.Entry(root, textvariable=frequency_var).grid(row=0, column=1, padx=10, pady=5)
159tk.Button(root, text="Set", command=send_frequency).grid(row=0, column=2, padx=10, pady=5)
160
161tk.Label(root, text="RDS PTY:",bg='#006400', fg=label_fg).grid(row=1, column=0, sticky=tk.E, padx=10, pady=5)
162
163# Combobox
164rds_pty_combobox = ttk.Combobox(root, textvariable=rds_pty_var, values=pty_descriptions)
165rds_pty_combobox.grid(row=1, column=1, padx=10, pady=5)
166tk.Button(root, text="Set", command=send_rds_pty).grid(row=1, column=2, padx=10, pady=5)
167
168tk.Label(root, text="RDS PS:", bg='#006400', fg=label_fg).grid(row=2, column=0, sticky=tk.E, padx=10, pady=5)
169tk.Entry(root, textvariable=rds_ps_var).grid(row=2, column=1, padx=10, pady=5)
170tk.Button(root, text="Set", command=send_rds_ps).grid(row=2, column=2, padx=10, pady=5)
171
172tk.Label(root, text="RDS RT:", bg='#006400', fg=label_fg).grid(row=3, column=0, sticky=tk.E, padx=10, pady=5)
173tk.Entry(root, textvariable=rds_rt_var).grid(row=3, column=1, padx=10, pady=5)
174tk.Button(root, text="Set", command=send_rds_rt).grid(row=3, column=2, padx=10, pady=5)
175
176tk.Label(root, text="Stereo/Mono:", bg='#006400', fg=label_fg).grid(row=4, column=0, sticky=tk.E, padx=10, pady=5)
177stereo_mono_combobox = ttk.Combobox(root, textvariable=stereo_mono_var, values= stereo_mono_descriptions)
178stereo_mono_combobox.grid(row=4, column=1, padx=10, pady=5)
179tk.Button(root, text="Set", command=send_stereo_mono).grid(row=4, column=2, padx=10, pady=5)
180
181tk.Label(root, text="Pre-Emphasis:", bg='#006400', fg=label_fg).grid(row=5, column=0, sticky=tk.E, padx=10, pady=5)
182pre_emphasis_combobox = ttk.Combobox(root, textvariable=pre_emphasis_var, values=pre_emphasis_descriptions)
183pre_emphasis_combobox.grid(row=5, column=1, padx=10, pady=5)
184tk.Button(root, text="Set", command=send_pre_emphasis).grid(row=5, column=2, padx=10, pady=5)
185
186tk.Label(root, text="Impedance:", bg='#006400', fg=label_fg).grid(row=6, column=0, sticky=tk.E, padx=10, pady=5)
187impedance_combobox = ttk.Combobox(root, textvariable=impedance_var, values = impedance_descriptions)
188
189impedance_combobox.grid(row=6, column=1, padx=10, pady=5)
190tk.Button(root, text="Set", command=send_impedance).grid(row=6, column=2, padx=10, pady=5)
191
192
193tk.Label(root, text="Buffer Gain:", bg='#006400', fg=label_fg).grid(row=7, column=0, sticky=tk.E, padx=10, pady=5)
194buffer_gain_combobox = ttk.Combobox(root, textvariable=buffer_gain_var, values = buffer_gain_descriptions)
195
196buffer_gain_combobox.grid(row=7, column=1, padx=10, pady=5)
197tk.Button(root, text="Set", command=send_buffer_gain).grid(row=7, column=2, padx=10, pady=5)
198
199tk.Label(root, text="Frequency Deviation (kHz):", bg='#006400', fg=label_fg).grid(row=8, column=0, sticky=tk.E, padx=10, pady=5)
200freq_dev_combobox = ttk.Combobox(root, textvariable=freq_dev_var)
201freq_dev_combobox['values'] = ['41.5', '60.0', '74.5','92.8','96.6', '110.4']
202freq_dev_combobox.grid(row=8, column=1, padx=10, pady=5)
203tk.Button(root, text="Set", command=send_freq_dev).grid(row=8, column=2, padx=10, pady=5)
204
205tk.Label(root, text="Soft Clip:", bg='#006400', fg=label_fg).grid(row=9, column=0, sticky=tk.E, padx=10, pady=5)
206soft_clip_combobox = ttk.Combobox(root, textvariable=soft_clip_var,values=soft_clip_descriptions)
207soft_clip_combobox.grid(row=9, column=1, padx=10, pady=5)
208tk.Button(root, text="Set", command=send_soft_clip).grid(row=9, column=2, padx=10, pady=5)
209
210tk.Label(root, text="Set Date and Time (YYYY/MM/DD HH:MM):", bg='#006400', fg=label_fg).grid(row=10, column=0, padx=10, pady=5)
211tk.Entry(root, textvariable=datetime_var).grid(row=10, column=1, padx=10, pady=5)
212tk.Button(root, text="Set", command=send_datetime).grid(row=10, column=2, padx=10, pady=5)
213
214
215# Start interface
216root.mainloop()
Downloadable files
Schematic
schematic_nano33_qn8066.jpg
Prototype – FM transmitter based on Nano 33 IoT and QN8066
prototype.jpg
Documentation
Arduino QN8066 FM DSP RX/TX Library
https://github.com/pu2clr/QN8066
Note: Content and images are from: https://projecthub.arduino.cc/, with some modifications.
If you want it removed due to copyright reasons, please leave a comment. Thank you.
I want to share this article more widely so that everyone knows about Arduino and your project.