diff --git a/README.md b/README.md index 56b5f64..68bfa3a 100644 --- a/README.md +++ b/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 = ""; const char* password = ""; -String api_url = "http://:5000/status"; +String api_base_url = "http://:5000"; ``` ## Error handling @@ -49,3 +50,47 @@ String api_url = "http://: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 + diff --git a/hr50_fonts.h b/client/hr50_fonts.h similarity index 100% rename from hr50_fonts.h rename to client/hr50_fonts.h diff --git a/hr50-remote-display.ino b/client/hr50_rd_client.ino similarity index 51% rename from hr50-remote-display.ino rename to client/hr50_rd_client.ino index 5a66e51..d1b975a 100644 --- a/hr50-remote-display.ino +++ b/client/hr50_rd_client.ino @@ -4,38 +4,45 @@ #include "heltec.h" #include #include "hr50_fonts.h" +#include -#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 = ""; const char* password = ""; String api_base_url = "http://: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() != "ERR") { + if ( status_json != NULL ) { // and status_json["PTT"].as() != "ERR") { String band = status_json["BND"].as(); String pep = status_json["PEP"].as(); String avg = status_json["AVG"].as(); String swr = status_json["SWR"].as(); - String voltage = status_json["VLT"].as(); + String vlt = status_json["VLT"].as(); String power = pep + "W/" + avg + "W"; String ptt = status_json["PTT"].as(); String temp = status_json["TMP"].as(); + String return_msg = status_json["RET"].as(); // 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(); + // print data to OLED screen print_status_screen(obj); - + // wait a bit + delay(500); } else { printError("No WiFi!"); } -} \ No newline at end of file +} diff --git a/server/__init__.py b/server/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/__pycache__/__init__.cpython-37.pyc b/server/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000..2c32286 Binary files /dev/null and b/server/__pycache__/__init__.cpython-37.pyc differ diff --git a/server/__pycache__/hr50_rd_server.cpython-37.pyc b/server/__pycache__/hr50_rd_server.cpython-37.pyc new file mode 100644 index 0000000..396286e Binary files /dev/null and b/server/__pycache__/hr50_rd_server.cpython-37.pyc differ diff --git a/server/__pycache__/hr50_server.cpython-37.pyc b/server/__pycache__/hr50_server.cpython-37.pyc new file mode 100644 index 0000000..14e72b6 Binary files /dev/null and b/server/__pycache__/hr50_server.cpython-37.pyc differ diff --git a/server/__pycache__/hr50api.cpython-37.pyc b/server/__pycache__/hr50api.cpython-37.pyc new file mode 100644 index 0000000..5fef6e6 Binary files /dev/null and b/server/__pycache__/hr50api.cpython-37.pyc differ diff --git a/server/bootstrap.sh b/server/bootstrap.sh new file mode 100755 index 0000000..a4413c4 --- /dev/null +++ b/server/bootstrap.sh @@ -0,0 +1,2 @@ +export FLASK_APP=hr50_rd_server.py +flask run --host=0.0.0.0 diff --git a/server/hr50_rd_server.py b/server/hr50_rd_server.py new file mode 100644 index 0000000..ebe1c49 --- /dev/null +++ b/server/hr50_rd_server.py @@ -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() + + diff --git a/server/scripts/toggle_antenna.sh b/server/scripts/toggle_antenna.sh new file mode 100755 index 0000000..345cbdc --- /dev/null +++ b/server/scripts/toggle_antenna.sh @@ -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