mirror of
https://codeberg.org/mclemens/hr50-remote-display.git
synced 2024-12-22 14:06:29 -05:00
moved client and server into a single project
fixed a lot of bugs added pushable button with long and short press added antenna toggle script
This commit is contained in:
parent
ecf516dd4b
commit
24fe5c873d
49
README.md
49
README.md
@ -15,6 +15,7 @@ This is how the display looks like in action:
|
||||
|
||||
![Photo of the Heltec board](hr50-remote-display.jpg "Photo of the Heltec board")
|
||||
|
||||
# Client
|
||||
|
||||
## Preconditions
|
||||
|
||||
@ -25,7 +26,6 @@ This is how the display looks like in action:
|
||||
### Software
|
||||
|
||||
* Arduino IDE
|
||||
* HR50-Api: [https://git.qrz.is/clemens/hr50-api]
|
||||
|
||||
### Libraries
|
||||
|
||||
@ -33,6 +33,7 @@ This is how the display looks like in action:
|
||||
* ArduinoJson
|
||||
* HTTPClient
|
||||
* WiFi
|
||||
* EasyButton
|
||||
|
||||
## Configuration
|
||||
|
||||
@ -41,7 +42,7 @@ Edit the following lines inside the Arduino sketch:
|
||||
```
|
||||
const char* ssid = "<ENTER_HERE_YOUR_WIFI_SSID>";
|
||||
const char* password = "<PASSWORD>";
|
||||
String api_url = "http://<ENTER_HERE_THE_IP_OF_YOUR API_SERVER>:5000/status";
|
||||
String api_base_url = "http://<ENTER_HERE_THE_IP_OF_YOUR API_SERVER>:5000";
|
||||
```
|
||||
|
||||
## Error handling
|
||||
@ -49,3 +50,47 @@ String api_url = "http://<ENTER_HERE_THE_IP_OF_YOUR API_SERVER>:5000/status";
|
||||
* If wifi is unavailable or misconfigured, the device will show "No Wifi!" on the screen
|
||||
* HTTP Errors will also displayed on the OLED
|
||||
* When you key up your TRX and therefore the HR50, no serial communication is possible. The latest gathered information will still be displayed on the screen but there will be a "(!)" in the third row as an indicator for this situation.
|
||||
|
||||
|
||||
# Server
|
||||
|
||||
# Preconditions
|
||||
|
||||
You need a computer connected to the Hardrock-50 via USB, e.g. a Raspberry Pi. This system needs to be able to run Python and has to be connected to your network.
|
||||
|
||||
# Configuration
|
||||
|
||||
Open the file _hr50_rd_server.py_ and adapt the following lines to your environment:
|
||||
|
||||
```
|
||||
serial_port = '/dev/ttyUSB0'
|
||||
baud = 19200
|
||||
```
|
||||
|
||||
# Execution
|
||||
|
||||
Executing this script will spawn a web server running on port 5000. This is not meant for production use and/or public exposure. Anyone able to access this server via the network can alter any settings on your HR50.
|
||||
|
||||
Run it as follows:
|
||||
|
||||
```
|
||||
# cd server
|
||||
# ./bootstrap.sh
|
||||
```
|
||||
|
||||
# API Endpoints
|
||||
|
||||
There are two API endpoints available:
|
||||
|
||||
#### /exec_serial
|
||||
|
||||
This method allows you to send commands to the HR50. The commands can be taken from the HR50 operator's manual and do not require the trailing ";"
|
||||
|
||||
#### /exec_shell
|
||||
|
||||
This method allows you to execute shell commands on the server
|
||||
|
||||
#### /get_status
|
||||
|
||||
This method returns all avalable information in JSON
|
||||
|
||||
|
@ -4,38 +4,45 @@
|
||||
#include "heltec.h"
|
||||
#include <ArduinoJson.h>
|
||||
#include "hr50_fonts.h"
|
||||
#include <EasyButton.h>
|
||||
|
||||
#define ESP_INTR_FLAG_DEFAULT 0
|
||||
#define PRG_BUTTON 0
|
||||
// Pin 0 is the built-in upper button
|
||||
#define BUTTON_PIN 0
|
||||
|
||||
EasyButton button(BUTTON_PIN);
|
||||
|
||||
const char* ssid = "<WIFI-SSID>";
|
||||
const char* password = "<WIFI-PASSWORD>";
|
||||
String api_base_url = "http://<IP_OF_HR50_API>:5000";
|
||||
//String api_button_url = api_base_url + "/?cmd=hrtu1";
|
||||
String api_button_url = api_base_url + "/exec";
|
||||
String api_status_url = api_base_url + "/status";
|
||||
|
||||
// Contruct API calls
|
||||
String api_button_long = api_base_url + "/exec_serial";
|
||||
String api_button_short = api_base_url + "/exec_shell";
|
||||
String api_status_url = api_base_url + "/get_status";
|
||||
|
||||
int first_row = 0;
|
||||
int second_row = 24;
|
||||
int third_row = 48;
|
||||
int left = 0;
|
||||
int right = 128;
|
||||
|
||||
String cmd_status = "";
|
||||
|
||||
SemaphoreHandle_t semaphore = nullptr;
|
||||
// milliseconds of button push until count as a long press
|
||||
int duration = 1000;
|
||||
|
||||
void IRAM_ATTR handler(void* arg) {
|
||||
xSemaphoreGiveFromISR(semaphore, NULL);
|
||||
// This happens when the button will be pressed long
|
||||
void long_button_press() {
|
||||
cmd_status = send_http_request(api_button_long);
|
||||
}
|
||||
|
||||
void button_task(void* arg) {
|
||||
for (;;) {
|
||||
if (xSemaphoreTake(semaphore, portMAX_DELAY) == pdTRUE) {
|
||||
cmd_status = send_http_request(api_button_url);
|
||||
}
|
||||
}
|
||||
// This happens when the button will be pressed short
|
||||
void short_button_press() {
|
||||
cmd_status = send_http_request(api_button_short);
|
||||
}
|
||||
|
||||
|
||||
// Sends a HTTP request to the server and grabs the response
|
||||
String send_http_request(String url) {
|
||||
HTTPClient http;
|
||||
String response = "";
|
||||
@ -53,31 +60,27 @@ String send_http_request(String url) {
|
||||
}
|
||||
|
||||
|
||||
// Things that need to be done after booting the device
|
||||
void setup() {
|
||||
// Initialize Heltec display
|
||||
Heltec.begin(true /*DisplayEnable Enable*/, false /*LoRa Disable*/, false /*Serial Enable*/);
|
||||
Heltec.display->flipScreenVertically();
|
||||
Heltec.display->setFont(DialogInput_plain_16);
|
||||
|
||||
|
||||
// Log into Wifi and wait a bit
|
||||
WiFi.begin(ssid, password);
|
||||
delay(4000);
|
||||
|
||||
// Configure PRG button of the Heltec
|
||||
semaphore = xSemaphoreCreateBinary();
|
||||
// Setup the button GPIO pin
|
||||
gpio_pad_select_gpio(PRG_BUTTON );
|
||||
// Quite obvious, a button is a input
|
||||
gpio_set_direction(GPIO_NUM_0, GPIO_MODE_INPUT);
|
||||
// Trigger the interrupt when going from HIGH -> LOW ( == pushing button)
|
||||
gpio_set_intr_type(GPIO_NUM_0, GPIO_INTR_NEGEDGE);
|
||||
// Associate button_task method as a callback
|
||||
xTaskCreate(button_task, "prg_button_task", 4096, NULL, 10, NULL);
|
||||
// Install ISR service
|
||||
gpio_install_isr_service(ESP_INTR_FLAG_DEFAULT);
|
||||
// Add button handler to the ISR
|
||||
gpio_isr_handler_add(GPIO_NUM_0, handler, NULL);
|
||||
|
||||
// Initialize the PRG button
|
||||
button.begin();
|
||||
// attach action to long button press
|
||||
button.onPressedFor(duration, long_button_press);
|
||||
// attach action to short button press
|
||||
button.onPressed(short_button_press);
|
||||
}
|
||||
|
||||
|
||||
// Print error message on the display
|
||||
void printError(String err) {
|
||||
Heltec.display->clear();
|
||||
Heltec.display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
@ -86,59 +89,54 @@ void printError(String err) {
|
||||
}
|
||||
|
||||
|
||||
// Extracts data from the server response, formats it
|
||||
// and writes it to the OLED
|
||||
void print_status_screen(JsonObject status_json) {
|
||||
|
||||
if ( status_json != NULL and status_json["PTT"].as<String>() != "ERR") {
|
||||
if ( status_json != NULL ) { // and status_json["PTT"].as<String>() != "ERR") {
|
||||
String band = status_json["BND"].as<String>();
|
||||
String pep = status_json["PEP"].as<String>();
|
||||
String avg = status_json["AVG"].as<String>();
|
||||
String swr = status_json["SWR"].as<String>();
|
||||
String voltage = status_json["VLT"].as<String>();
|
||||
String vlt = status_json["VLT"].as<String>();
|
||||
String power = pep + "W/" + avg + "W";
|
||||
String ptt = status_json["PTT"].as<String>();
|
||||
String temp = status_json["TMP"].as<String>();
|
||||
String return_msg = status_json["RET"].as<String>();
|
||||
|
||||
// clear the display
|
||||
Heltec.display->clear();
|
||||
// print left column
|
||||
Heltec.display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
Heltec.display->drawString(left, first_row, band );
|
||||
Heltec.display->drawString(left, second_row, power );
|
||||
if ( cmd_status == "" ) {
|
||||
Heltec.display->drawString(left, third_row, voltage );
|
||||
} else {
|
||||
Heltec.display->drawString(left, third_row, cmd_status );
|
||||
}
|
||||
Heltec.display->drawString(left, first_row, band ); // top row
|
||||
Heltec.display->drawString(left, second_row, power ); // mid row
|
||||
Heltec.display->drawString(left, third_row, return_msg ); // bottom row
|
||||
// print right column
|
||||
Heltec.display->setTextAlignment(TEXT_ALIGN_RIGHT);
|
||||
Heltec.display->drawString(right, first_row, swr );
|
||||
Heltec.display->drawString(right, second_row, ptt );
|
||||
Heltec.display->drawString(right, third_row, temp );
|
||||
Heltec.display->drawString(right, first_row, swr ); // top row
|
||||
Heltec.display->drawString(right, second_row, ptt ); // mid row
|
||||
Heltec.display->drawString(right, third_row, temp ); // bottom row
|
||||
Heltec.display->display();
|
||||
}
|
||||
else
|
||||
{
|
||||
// print a "(!)" centered in the third row
|
||||
// this happens when the TRX/PA is transmitting
|
||||
// or otherwise an empty response was send by the API
|
||||
Heltec.display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
Heltec.display->drawString(58, third_row, "(!)" );
|
||||
Heltec.display->display();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void loop() {
|
||||
//Check WiFi connection status
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
// read out button state
|
||||
button.read();
|
||||
// get status data from server
|
||||
String response = send_http_request(api_status_url);
|
||||
// The server sends the status data as json string
|
||||
// This converts the string to a json object
|
||||
StaticJsonDocument<200> doc;
|
||||
deserializeJson(doc, response);
|
||||
JsonObject obj = doc.as<JsonObject>();
|
||||
// print data to OLED screen
|
||||
print_status_screen(obj);
|
||||
|
||||
// wait a bit
|
||||
delay(500);
|
||||
}
|
||||
else {
|
||||
printError("No WiFi!");
|
||||
}
|
||||
}
|
||||
}
|
0
server/__init__.py
Normal file
0
server/__init__.py
Normal file
BIN
server/__pycache__/__init__.cpython-37.pyc
Normal file
BIN
server/__pycache__/__init__.cpython-37.pyc
Normal file
Binary file not shown.
BIN
server/__pycache__/hr50_rd_server.cpython-37.pyc
Normal file
BIN
server/__pycache__/hr50_rd_server.cpython-37.pyc
Normal file
Binary file not shown.
BIN
server/__pycache__/hr50_server.cpython-37.pyc
Normal file
BIN
server/__pycache__/hr50_server.cpython-37.pyc
Normal file
Binary file not shown.
BIN
server/__pycache__/hr50api.cpython-37.pyc
Normal file
BIN
server/__pycache__/hr50api.cpython-37.pyc
Normal file
Binary file not shown.
2
server/bootstrap.sh
Executable file
2
server/bootstrap.sh
Executable file
@ -0,0 +1,2 @@
|
||||
export FLASK_APP=hr50_rd_server.py
|
||||
flask run --host=0.0.0.0
|
179
server/hr50_rd_server.py
Normal file
179
server/hr50_rd_server.py
Normal file
@ -0,0 +1,179 @@
|
||||
from flask import Flask, jsonify, request
|
||||
import sys
|
||||
import getopt
|
||||
import time
|
||||
import serial
|
||||
from subprocess import check_output
|
||||
import threading
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
shell_cmd = "./scripts/toggle_antenna.sh"
|
||||
serial_cmd = "hrtu1"
|
||||
|
||||
# The serial/USB port the HR50 is connected to
|
||||
serial_port = '/dev/ttyUSB0'
|
||||
|
||||
# doesn't really matter but must match settings of the HR50
|
||||
baud = 19200
|
||||
|
||||
# the active command that has been sent via the API
|
||||
# If populated: next connection to the HR50 will be used
|
||||
# to send the command
|
||||
# If empty: next connection to the HR50 will be used
|
||||
# to query status information
|
||||
cmd_rcvd = ""
|
||||
|
||||
# time in milliseconds when we started to display a status message
|
||||
msg_time = 0
|
||||
|
||||
# time in milliseconds a status message will be displayed
|
||||
msg_duration = 3000
|
||||
|
||||
'''
|
||||
bands = {
|
||||
'6': '0',
|
||||
'10': '1',
|
||||
'12': '2',
|
||||
'15': '3',
|
||||
'17': '4',
|
||||
'20': '5',
|
||||
'30': '6',
|
||||
'40': '7',
|
||||
'60': '8',
|
||||
'80': '9',
|
||||
'160': '10'
|
||||
}
|
||||
'''
|
||||
|
||||
vlcd = {
|
||||
'STA': '-',
|
||||
'PTT': '-',
|
||||
'BND': '-',
|
||||
'VLT': '-',
|
||||
'PEP': '-',
|
||||
'AVG': '-',
|
||||
'SWR': '-',
|
||||
'TMP': '-',
|
||||
'RET': 'ok'
|
||||
}
|
||||
|
||||
'''
|
||||
keying_methods = {
|
||||
"off" : "0",
|
||||
"ptt" : "1",
|
||||
"cor" : "2",
|
||||
"qrp" : "3"
|
||||
}
|
||||
'''
|
||||
|
||||
def current_milli_time():
|
||||
return round(time.time() * 1000)
|
||||
|
||||
|
||||
def get_serial(port, baud):
|
||||
try:
|
||||
ser = serial.Serial(port, baud, timeout=2)
|
||||
return ser
|
||||
except serial.serialutil.SerialException as e:
|
||||
print("The following error has occured while opening the serial connecti on to {} with {} baud:".format(port, baud))
|
||||
print(e)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
# sends a command to the
|
||||
# Hardrock-50 via the serial interface
|
||||
def send_cmd_via_serial(cmd):
|
||||
ser = get_serial(serial_port, baud)
|
||||
res = None
|
||||
try:
|
||||
command = cmd + ';'
|
||||
ser.write(str.encode(command))
|
||||
res = ser.readline().decode("utf-8").rstrip()
|
||||
except Exception as e:
|
||||
print("The following error has occured while sending the command {} to t he HR50:".format(port, baud))
|
||||
print(e)
|
||||
ser.close()
|
||||
return res
|
||||
|
||||
|
||||
# Executed two commands vie the serial interface,
|
||||
# parsed the result ad polulates a dict with the
|
||||
# collected information
|
||||
def get_info():
|
||||
global msg_time
|
||||
ser = get_serial(serial_port, baud)
|
||||
try:
|
||||
ser.write(b'HRRX;')
|
||||
time.sleep(0.5)
|
||||
res = ser.readline().decode("utf-8").rstrip().replace(';', '').split(',')
|
||||
except Exception as e:
|
||||
print("The following error has occured while sending the command {} to the HR50:".format(port, baud))
|
||||
print(e)
|
||||
ser.close()
|
||||
return None
|
||||
if res and len(res) > 3:
|
||||
vlcd['STA'] = res[0]
|
||||
vlcd['PTT'] = res[1]
|
||||
vlcd['BND'] = res[2]
|
||||
vlcd['TMP'] = res[3]
|
||||
vlcd['VLT'] = res[4]
|
||||
ser.write(b'HRMX;')
|
||||
time.sleep(0.5)
|
||||
res = ser.readline().decode("utf-8").rstrip().split()
|
||||
ser.close()
|
||||
vlcd['PEP'] = res[1][1:]
|
||||
vlcd['AVG'] = res[2][1:]
|
||||
if res[3][1:] != "0":
|
||||
vlcd['SWR'] = res[3][1:2] + "." + res[3][2:3]
|
||||
else:
|
||||
vlcd['RET'] = "(!)"
|
||||
msg_time = current_milli_time()
|
||||
|
||||
|
||||
def background_loop():
|
||||
global cmd_rcvd
|
||||
global msg_time
|
||||
threading.Timer(3.0, background_loop).start()
|
||||
if cmd_rcvd != "":
|
||||
ret = ""
|
||||
try:
|
||||
send_cmd_via_serial(cmd_rcvd)
|
||||
vlcd['RET'] = 'Tune'
|
||||
msg_time = current_milli_time()
|
||||
except Exception as e:
|
||||
ret = "ERROR"
|
||||
cmd_rcvd = ""
|
||||
else:
|
||||
get_info()
|
||||
if msg_time != 0 and msg_time + msg_duration < current_milli_time() or vlcd['RET'] == "ok":
|
||||
vlcd['RET'] = vlcd['VLT']
|
||||
|
||||
|
||||
@app.route('/get_status')
|
||||
def get_status():
|
||||
return jsonify(vlcd)
|
||||
|
||||
|
||||
@app.route('/exec_shell')
|
||||
def exec_shell_command():
|
||||
global msg_time
|
||||
out = check_output([shell_cmd, ""])
|
||||
if out:
|
||||
vlcd['RET'] = out.decode("utf-8")
|
||||
msg_time = current_milli_time()
|
||||
return out
|
||||
|
||||
|
||||
@app.route('/exec_serial')
|
||||
def exec_serial_command():
|
||||
global cmd_rcvd
|
||||
cmd_rcvd = serial_cmd
|
||||
return jsonify(isError= False,
|
||||
message= "Success",
|
||||
statusCode= 200,
|
||||
data= "" ), 200
|
||||
|
||||
background_loop()
|
||||
|
||||
|
16
server/scripts/toggle_antenna.sh
Executable file
16
server/scripts/toggle_antenna.sh
Executable file
@ -0,0 +1,16 @@
|
||||
#!/usr/bin/bash
|
||||
eval $(sudo usbrelay 2>/dev/null)
|
||||
#echo $BITFT_1
|
||||
if [ $BITFT_1 -eq 0 ]
|
||||
then
|
||||
sudo usbrelay BITFT_1=1 > /dev/null 2>&1
|
||||
else
|
||||
sudo usbrelay BITFT_1=0 > /dev/null 2>&1
|
||||
fi
|
||||
eval $(sudo usbrelay 2>/dev/null)
|
||||
if [ $BITFT_1 -eq 0 ]
|
||||
then
|
||||
echo "Cobweb"
|
||||
else
|
||||
echo "Vertical"
|
||||
fi
|
Loading…
Reference in New Issue
Block a user