Compare commits
87 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 269cbfdd22 | |||
| c48c624f71 | |||
| dccd3d27f4 | |||
| 2618d82e12 | |||
| 08b1bdf3fd | |||
| d973bbf19a | |||
| f4287eba7b | |||
| 3474d8ba17 | |||
| ffb4163980 | |||
| 3db888e530 | |||
| a93498ac84 | |||
| 6c1a718a98 | |||
| 4380a56055 | |||
| f5aa43ef76 | |||
| c8f9d823da | |||
| a47cd1465a | |||
| 73a6b694a9 | |||
| 06e69bd8c0 | |||
| dd085635a2 | |||
| 087ad27c8c | |||
| 5c655a381b | |||
| dc765af473 | |||
| 3aba0583be | |||
| 1bde39e148 | |||
| 958e71513c | |||
| 70decbbcca | |||
| 2127d4a75a | |||
| 40baa679e1 | |||
| e706327623 | |||
| fbe5a4a6e0 | |||
| 26a1ee97c0 | |||
| cbdc7ec939 | |||
| 38c1417351 | |||
| 57a1c1af80 | |||
| af4920d634 | |||
| a69128397a | |||
| f496a10ef2 | |||
| afe5b9338d | |||
| a996c13e63 | |||
| dab1590608 | |||
| 90b3137165 | |||
| 55ea853100 | |||
| b97f48858d | |||
| b648900c7b | |||
| c174d7f594 | |||
| fd61efebba | |||
| 8a95413224 | |||
| 458232f08f | |||
| a47451b541 | |||
| 2060df9691 | |||
| c65bc22028 | |||
| 6807d3f56f | |||
| 07db0cf748 | |||
| 4c6105734e | |||
| a3ec425c88 | |||
| 48db6bc4c9 | |||
| d4db1fa1f8 | |||
| d5852bb3bb | |||
| abe751d30c | |||
| 0317a93d38 | |||
| ca53478b3c | |||
| 736109b9b0 | |||
| 5f6cfd653c | |||
| 7d673fe70c | |||
| bfc43443b4 | |||
| 20e075d29d | |||
| 6af5dfcd0e | |||
| cc4d798a03 | |||
| 50eaf8e973 | |||
| b0be20087c | |||
| 13ddf853ed | |||
| 17d8883f39 | |||
| 6552d9034f | |||
| 9b928cb11b | |||
| b673520a39 | |||
| 3977971341 | |||
| 7f4c8c001e | |||
| e573c1ed1b | |||
| 917dd85465 | |||
| 5b9e369e7a | |||
| 856ada13e8 | |||
| 76ef0b129f | |||
| df8e3af432 | |||
| d2e0b6ef97 | |||
| 969680fba3 | |||
| ab7ea696ca | |||
| d096346247 |
@@ -1,4 +0,0 @@
|
||||
/logs/*
|
||||
config.ini
|
||||
*.db
|
||||
*.log
|
||||
@@ -1,80 +0,0 @@
|
||||
# README
|
||||
|
||||
I got tired of APRS-IS websites not showing me all the paths that packets
|
||||
take, only the shortest path to IS. So this is a Python 3 tool that turns
|
||||
direwolf logs into a REST API in JSON format.
|
||||
|
||||
## Setup
|
||||
1. Run direwolf with logging to CSV on by using `-l`. (`-L` not yet implemented).
|
||||
1. Install requirements using `pip install -r requirements.txt`.
|
||||
1. Set up database file with `python init_db.py`.
|
||||
2. Run `app.py` with either a Python call or a real WSGI server.
|
||||
You can use screen to detach the session.
|
||||
- Default URL is http://127.0.0.1:5001
|
||||
- Example `waitress` and `screen` scripts are included, see
|
||||
- `api_waitress.py` and
|
||||
- `start-aprs_api.sh`
|
||||
3. Access the API from whatever other system you want.
|
||||
|
||||
## Endpoints:
|
||||
-`/packets` - gives the most recent packets, sorted descending by time received.
|
||||
- argument `n` will return a specific number of packets, default 10. E.g.,
|
||||
`https://digi.w1cdn.net/aprs_api/packets?n=1` returns one packet.
|
||||
- argument `from` will return packets from the named station-SSID (no wildcards).
|
||||
E.g., `https://digi.w1cdn.net/aprs_api/packets?n=1&from=W1CDN-1` returns
|
||||
one packet from W1CDN-1.
|
||||
|
||||
Example of an object packet sent by W1CDN-1 and digipeated by K0UND-2:
|
||||
```
|
||||
{
|
||||
"id": 1,
|
||||
"addresse": null,
|
||||
"alive": null,
|
||||
"altitude": null,
|
||||
"comment": "Leave a message to say hi!",
|
||||
"course": null,
|
||||
"created": "2023-04-16 15:04:03",
|
||||
"format": "uncompressed",
|
||||
"frame": null,
|
||||
"from": "W1CDN-2",
|
||||
"gpsfixstatus": null,
|
||||
"latitude": 47.94133333333333,
|
||||
"longitude": -97.02683333333333,
|
||||
"mbits": null,
|
||||
"messagecapable": 1,
|
||||
"message_text": null,
|
||||
"mtype": null,
|
||||
"object_format": null,
|
||||
"object_name": null,
|
||||
"path": "['K0UND-2', 'WIDE2-2']",
|
||||
"phg": null,
|
||||
"phg_dir": null,
|
||||
"phg_gain": null,
|
||||
"phg_height": null,
|
||||
"phg_power": null,
|
||||
"phg_range": null,
|
||||
"posambiguity": 0,
|
||||
"raw": "W1CDN-2>APQTH1,K0UND-2,WIDE2-2:@150321h4756.48N/09701.61W-Leave a message to say hi!",
|
||||
"raw_timestamp": "150321h",
|
||||
"speed": null,
|
||||
"station_call": "W1CDN-1",
|
||||
"station_lat": 47.9415,
|
||||
"station_lon": -97.027,
|
||||
"status": null,
|
||||
"symbol": "-",
|
||||
"symbol_table": "/",
|
||||
"telemetry": null,
|
||||
"timestamp": 1681657401,
|
||||
"to": "APQTH1",
|
||||
"tEQNS": null,
|
||||
"tPARM": null,
|
||||
"tUNIT": null,
|
||||
"via": "",
|
||||
"weather": null,
|
||||
"wx_raw_timestamp": null
|
||||
}
|
||||
```
|
||||
|
||||
# Contributing
|
||||
If you want to contribute, please get in touch with me on Mastodon at
|
||||
https://mastodon.radio/@W1CDN.
|
||||
-187
@@ -1,187 +0,0 @@
|
||||
from flask import Flask, request, render_template
|
||||
from flask_restful import Resource, Api, reqparse, url_for
|
||||
from datetime import date, timedelta
|
||||
import configparser
|
||||
import csv
|
||||
import datetime
|
||||
import ast
|
||||
import glob
|
||||
import json, operator
|
||||
import requests
|
||||
import sqlite3
|
||||
api_app = Flask(__name__)
|
||||
api = Api(api_app)
|
||||
|
||||
# TODO this is duplicated from kiss_and_db.py, can I avoid that?
|
||||
import constants
|
||||
|
||||
def read_config():
|
||||
config = configparser.ConfigParser()
|
||||
config.read('config.ini')
|
||||
return config
|
||||
|
||||
def read_logs(log_folder):
|
||||
# Read some log files
|
||||
# UTC time, so let's look at tomorrow, today, and yesterday.
|
||||
today = date.today()
|
||||
yesterday = today - timedelta(days = 1)
|
||||
tomorrow = today + timedelta(days = 1)
|
||||
file_list = glob.glob(log_folder+str(yesterday)+"*") + \
|
||||
glob.glob(log_folder+str(today)+"*") + \
|
||||
glob.glob(log_folder+str(tomorrow)+"*")
|
||||
|
||||
# https://stackoverflow.com/a/66071962
|
||||
json_array = []
|
||||
for file in file_list:
|
||||
with open(file, encoding='utf-8') as csvf:
|
||||
csvReader = csv.DictReader(csvf)
|
||||
for row in csvReader:
|
||||
#add this python dict to json array
|
||||
json_array.append(row)
|
||||
|
||||
# Add the call and location of this station to the packet info
|
||||
config = read_config()
|
||||
for item in json_array:
|
||||
item['station_name'] = config['Settings']['station_call']
|
||||
item['station_lat'] = config['Settings']['station_lat']
|
||||
item['station_lon'] = config['Settings']['station_lon']
|
||||
|
||||
return(json_array)
|
||||
|
||||
def dict_factory(cursor, row):
|
||||
d = {}
|
||||
for idx, col in enumerate(cursor.description):
|
||||
d[col[0]] = row[idx]
|
||||
return d
|
||||
|
||||
def get_db_connection():
|
||||
conn = sqlite3.connect('database.db')
|
||||
conn.row_factory = dict_factory
|
||||
return conn
|
||||
|
||||
def select_all_frames(conn):
|
||||
"""
|
||||
Query all rows in the frames table
|
||||
:param conn: the Connection object
|
||||
:return:
|
||||
"""
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT * FROM frames")
|
||||
rows = cur.fetchall()
|
||||
return rows
|
||||
|
||||
def select_all_stations(conn):
|
||||
"""
|
||||
Query all rows in the stations table
|
||||
:param conn: the Connection object
|
||||
:return:
|
||||
"""
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT * FROM stations ORDER BY last_heard_unix DESC")
|
||||
rows = cur.fetchall()
|
||||
return rows
|
||||
|
||||
def unique_stations(conn):
|
||||
"""
|
||||
Query all rows in the frames table
|
||||
:param conn: the Connection object
|
||||
:return:
|
||||
"""
|
||||
cur = conn.cursor()
|
||||
cur.execute('SELECT *, MAX(id), COUNT(id) FROM frames GROUP BY "from" ORDER BY MAX(id) DESC')
|
||||
rows = cur.fetchall()
|
||||
return rows
|
||||
|
||||
def select_frames(conn, n, url_params):
|
||||
|
||||
# Should pass this a dict of fields and values (request.args)
|
||||
# TODO clean data before sending to DB
|
||||
# Filter out any keys that don't match db fields
|
||||
# From https://stackoverflow.com/a/20256491
|
||||
dictfilt = lambda x, y: dict([ (i,x[i]) for i in x if i in set(y) ])
|
||||
field_where = dictfilt(url_params, constants.db_frames_fields)
|
||||
# Then loop through fields to create query parts
|
||||
# From https://stackoverflow.com/a/73512269/2152245
|
||||
field_where_str = ' AND '.join([f'"{k}" LIKE \'{v}\'' for k,v in field_where.items()])
|
||||
|
||||
cur = conn.cursor()
|
||||
# Workaround to deal with missing value in WHERE
|
||||
field_where_query = "" if field_where_str == "" else "WHERE "+field_where_str
|
||||
sql = 'SELECT * FROM frames {field_where_query} ORDER BY created DESC LIMIT {n}'.format(field_where_query=field_where_query, n=n)
|
||||
print(sql)
|
||||
cur.execute(sql)
|
||||
rows = cur.fetchall()
|
||||
return rows
|
||||
|
||||
@api_app.route('/')
|
||||
def index():
|
||||
|
||||
# Get list of recent packets using API
|
||||
# TODO use relative path
|
||||
response = json.loads(requests.get("https://digi.w1cdn.net/aprs_api/packets").text)['data']
|
||||
|
||||
# Play with function to create station list
|
||||
#stations = select_all_stations(get_db_connection())
|
||||
#print(url_for("static", filename="test.txt", _external=True))
|
||||
# this should work: stations = json.loads(requests.get(url_for("stations", _external=True)).text)['data']
|
||||
stations = json.loads(requests.get("https://digi.w1cdn.net/aprs_api/stations").text)['data']
|
||||
# Convert unix time to datetime on the fly because I'm lazy right now
|
||||
for station in stations:
|
||||
if station['last_heard_unix'] != None:
|
||||
station['last_heard'] = datetime.datetime.utcfromtimestamp(station['last_heard_unix'])
|
||||
|
||||
return render_template('index.html',
|
||||
station_call = config['Settings']['station_call'],
|
||||
station_lat = config['Settings']['station_lat'],
|
||||
station_lon = config['Settings']['station_lon'],
|
||||
frames = response,
|
||||
stations = stations)
|
||||
|
||||
class Packets(Resource):
|
||||
def get(self):
|
||||
# Handle arguments that may or may not exist
|
||||
try:
|
||||
n = int(request.args.get('n'))
|
||||
except:
|
||||
n = 10
|
||||
|
||||
conn = get_db_connection()
|
||||
# Limit to number of records requested
|
||||
data = select_frames(conn, n = n, url_params = request.args.to_dict())
|
||||
# Sort by created date, descending (https://stackoverflow.com/a/45266808)
|
||||
#data.sort(key=operator.itemgetter('created'), reverse=True)
|
||||
return {'data':data}, 200 # return data and 200 OK code
|
||||
|
||||
class Stations(Resource):
|
||||
def get(self):
|
||||
# Handle arguments that may or may not exist
|
||||
try:
|
||||
n = int(request.args.get('n'))
|
||||
except:
|
||||
n = 10
|
||||
|
||||
conn = get_db_connection()
|
||||
# Limit to number of records requested
|
||||
data = select_all_stations(conn)
|
||||
# Sort by created date, descending (https://stackoverflow.com/a/45266808)
|
||||
#data.sort(key=operator.itemgetter('created'), reverse=True)
|
||||
return {'data':data}, 200 # return data and 200 OK code
|
||||
|
||||
# Read config
|
||||
config = read_config()
|
||||
log_folder = config['Settings']['log_folder']
|
||||
|
||||
# Start subprocess to watch KISS connection
|
||||
import subprocess
|
||||
#proc = subprocess.Popen(["python3","kiss_and_db.py"])
|
||||
# Combine under one process https://stackoverflow.com/a/13143013/2152245
|
||||
proc = subprocess.Popen("exec " + "python3 kiss_and_db.py", stdout=subprocess.PIPE, shell=True)
|
||||
print("kiss_and_db.py as subprocess pid "+str(proc.pid))
|
||||
|
||||
# The packets endpoint
|
||||
api.add_resource(Packets, '/packets')
|
||||
# The stations endpoint
|
||||
api.add_resource(Stations, '/stations')
|
||||
|
||||
if __name__ == '__main__':
|
||||
api_app.run(debug=True, host='0.0.0.0', port=5001) # run our Flask app
|
||||
@@ -1,12 +0,0 @@
|
||||
# run.py from https://www.devdungeon.com/content/run-python-wsgi-web-app-waitress
|
||||
import os
|
||||
from waitress import serve
|
||||
from api_app import api_app # Import your app
|
||||
|
||||
# Run from the same directory as this script
|
||||
this_files_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
os.chdir(this_files_dir)
|
||||
|
||||
# `url_prefix` is optional, but useful if you are serving app on a sub-dir
|
||||
# behind a reverse-proxy.
|
||||
serve(api_app, host='0.0.0.0', port=5001)
|
||||
@@ -1,20 +0,0 @@
|
||||
[Settings]
|
||||
# Path to direwolf log folder, include trailing slash
|
||||
log_folder = logs/
|
||||
#log_folder = /home/pi/logs/direwolf/
|
||||
|
||||
# Name and location of this station, for inclusion in the API
|
||||
station_call = W1CDN-1
|
||||
station_lat = 47.941500
|
||||
station_lon = -97.027000
|
||||
|
||||
# How long to keep packets (frames) e.g., "2 days", "5 minutes"
|
||||
keep_time = "2 days"
|
||||
|
||||
# KISS settings
|
||||
kiss_host = 192.168.0.30
|
||||
kiss_port = 8001
|
||||
|
||||
# Development settings (not operational yet)
|
||||
mycall = W1CDN-15
|
||||
log_path = aprs_api.log
|
||||
@@ -1,49 +0,0 @@
|
||||
# Tuple of frames table fields
|
||||
db_frames_fields = ("id",
|
||||
"addresse",
|
||||
"alive",
|
||||
"altitude",
|
||||
"comment",
|
||||
"course",
|
||||
"created",
|
||||
"created_unix",
|
||||
"format",
|
||||
"frame",
|
||||
"from",
|
||||
"gpsfixstatus",
|
||||
"latitude",
|
||||
"longitude",
|
||||
"mbits",
|
||||
"messagecapable",
|
||||
"message_text",
|
||||
"msgNo",
|
||||
"mtype",
|
||||
"object_format",
|
||||
"object_name",
|
||||
"path",
|
||||
"phg",
|
||||
"phg_dir",
|
||||
"phg_gain",
|
||||
"phg_height",
|
||||
"phg_power",
|
||||
"phg_range",
|
||||
"posambiguity",
|
||||
"raw",
|
||||
"raw_timestamp",
|
||||
"speed",
|
||||
"station_call",
|
||||
"station_lat",
|
||||
"station_lon",
|
||||
"status",
|
||||
"subpacket",
|
||||
"symbol",
|
||||
"symbol_table",
|
||||
"telemetry",
|
||||
"timestamp",
|
||||
"to",
|
||||
"tEQNS",
|
||||
"tPARM",
|
||||
"tUNIT",
|
||||
"via",
|
||||
"weather",
|
||||
"wx_raw_timestamp")
|
||||
@@ -0,0 +1,183 @@
|
||||
/*********
|
||||
Rui Santos
|
||||
Complete project details at https://RandomNerdTutorials.com/esp32-esp8266-input-data-html-form/
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files.
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
*********/
|
||||
|
||||
// include wifi password
|
||||
#include "config.h"
|
||||
#include <Arduino.h>
|
||||
#include <WiFi.h>
|
||||
#include <AsyncTCP.h>
|
||||
#include <SPIFFS.h>
|
||||
#include <Preferences.h>
|
||||
|
||||
// download zip from https://github.com/me-no-dev/ESPAsyncWebServer and install.
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
AsyncWebServer server(80);
|
||||
|
||||
// Read from config.h
|
||||
const char* ssid = WIFI_SSID;
|
||||
const char* password = WIFI_PASSWORD;
|
||||
|
||||
const char* PARAM_STRING = "inputString";
|
||||
const char* PARAM_INT = "inputInt";
|
||||
const char* PARAM_FLOAT = "inputFloat";
|
||||
|
||||
// HTML web page to handle 3 input fields (inputString, inputInt, inputFloat)
|
||||
const char index_html[] PROGMEM = R"rawliteral(
|
||||
<!DOCTYPE HTML><html><head>
|
||||
<title>ESP Input Form</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<!--<script>
|
||||
function submitMessage() {
|
||||
alert("Saved value to ESP SPIFFS");
|
||||
setTimeout(function(){ document.location.reload(false); }, 500);
|
||||
}
|
||||
</script>-->
|
||||
</head><body>
|
||||
<form action="/get">
|
||||
inputString (current value %inputString%): <input type="text" name="inputString">
|
||||
<!--<input type="submit" value="Submit" ">
|
||||
</form>--><br>
|
||||
<!--<form action="/get" target="hidden-form">-->
|
||||
inputInt (current value %inputInt%): <input type="number " name="inputInt">
|
||||
<!--<input type="submit" value="Submit" ">
|
||||
</form>--><br>
|
||||
<!--<form action="/get" target="hidden-form">-->
|
||||
inputFloat (current value %inputFloat%): <input type="number " name="inputFloat">
|
||||
<input type="submit" value="Submit" ">
|
||||
</form>
|
||||
<!--<iframe style="display:none" name="hidden-form"></iframe>-->
|
||||
</body></html>)rawliteral";
|
||||
|
||||
void notFound(AsyncWebServerRequest *request) {
|
||||
request->send(404, "text/plain", "Not found");
|
||||
}
|
||||
|
||||
String readFile(fs::FS &fs, const char * path){
|
||||
Serial.printf("Reading file: %s\r\n", path);
|
||||
File file = fs.open(path, "r");
|
||||
if(!file || file.isDirectory()){
|
||||
Serial.println("- empty file or failed to open file");
|
||||
return String();
|
||||
}
|
||||
Serial.println("- read from file:");
|
||||
String fileContent;
|
||||
while(file.available()){
|
||||
fileContent+=String((char)file.read());
|
||||
}
|
||||
file.close();
|
||||
Serial.println(fileContent);
|
||||
return fileContent;
|
||||
}
|
||||
|
||||
void writeFile(fs::FS &fs, const char * path, const char * message){
|
||||
Serial.printf("Writing file: %s\r\n", path);
|
||||
File file = fs.open(path, "w");
|
||||
if(!file){
|
||||
Serial.println("- failed to open file for writing");
|
||||
return;
|
||||
}
|
||||
if(file.print(message)){
|
||||
Serial.println("- file written");
|
||||
} else {
|
||||
Serial.println("- write failed");
|
||||
}
|
||||
file.close();
|
||||
}
|
||||
|
||||
// Replaces placeholder with stored values
|
||||
String processor(const String& var){
|
||||
//Serial.println(var);
|
||||
if(var == "inputString"){
|
||||
return readFile(SPIFFS, "/inputString.txt");
|
||||
}
|
||||
else if(var == "inputInt"){
|
||||
return readFile(SPIFFS, "/inputInt.txt");
|
||||
}
|
||||
else if(var == "inputFloat"){
|
||||
return readFile(SPIFFS, "/inputFloat.txt");
|
||||
}
|
||||
return String();
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
// Initialize SPIFFS
|
||||
#ifdef ESP32
|
||||
if(!SPIFFS.begin(true)){
|
||||
Serial.println("An Error has occurred while mounting SPIFFS");
|
||||
return;
|
||||
}
|
||||
#else
|
||||
if(!SPIFFS.begin()){
|
||||
Serial.println("An Error has occurred while mounting SPIFFS");
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(ssid, password);
|
||||
if (WiFi.waitForConnectResult() != WL_CONNECTED) {
|
||||
Serial.println("WiFi Failed!");
|
||||
return;
|
||||
}
|
||||
Serial.println();
|
||||
Serial.print("IP Address: ");
|
||||
Serial.println(WiFi.localIP());
|
||||
|
||||
// Send web page with input fields to client
|
||||
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||
request->send_P(200, "text/html", index_html, processor);
|
||||
});
|
||||
|
||||
// Send a GET request to <ESP_IP>/get?inputString=<inputMessage>
|
||||
server.on("/get", HTTP_GET, [] (AsyncWebServerRequest *request) {
|
||||
String inputMessage;
|
||||
// GET inputString value on <ESP_IP>/get?inputString=<inputMessage>
|
||||
if (request->hasParam(PARAM_STRING)) {
|
||||
inputMessage = request->getParam(PARAM_STRING)->value();
|
||||
writeFile(SPIFFS, "/inputString.txt", inputMessage.c_str());
|
||||
}
|
||||
// GET inputInt value on <ESP_IP>/get?inputInt=<inputMessage>
|
||||
else if (request->hasParam(PARAM_INT)) {
|
||||
inputMessage = request->getParam(PARAM_INT)->value();
|
||||
writeFile(SPIFFS, "/inputInt.txt", inputMessage.c_str());
|
||||
}
|
||||
// GET inputFloat value on <ESP_IP>/get?inputFloat=<inputMessage>
|
||||
else if (request->hasParam(PARAM_FLOAT)) {
|
||||
inputMessage = request->getParam(PARAM_FLOAT)->value();
|
||||
writeFile(SPIFFS, "/inputFloat.txt", inputMessage.c_str());
|
||||
}
|
||||
else {
|
||||
inputMessage = "No message sent";
|
||||
}
|
||||
Serial.println(inputMessage);
|
||||
request->send(200, "text/text", inputMessage);
|
||||
});
|
||||
server.onNotFound(notFound);
|
||||
server.begin();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// To access your stored values on inputString, inputInt, inputFloat
|
||||
String yourInputString = readFile(SPIFFS, "/inputString.txt");
|
||||
Serial.print("*** Your inputString: ");
|
||||
Serial.println(yourInputString);
|
||||
|
||||
int yourInputInt = readFile(SPIFFS, "/inputInt.txt").toInt();
|
||||
Serial.print("*** Your inputInt: ");
|
||||
Serial.println(yourInputInt);
|
||||
|
||||
float yourInputFloat = readFile(SPIFFS, "/inputFloat.txt").toFloat();
|
||||
Serial.print("*** Your inputFloat: ");
|
||||
Serial.println(yourInputFloat);
|
||||
delay(5000);
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
/*********
|
||||
Rui Santos
|
||||
Complete project details at https://randomnerdtutorials.com
|
||||
*********/
|
||||
// include wifi password
|
||||
#include "config.h"
|
||||
// Load Wi-Fi library
|
||||
#include <WiFi.h>
|
||||
// Load Preferences library
|
||||
#include <Preferences.h>
|
||||
// Define preferences instance
|
||||
Preferences prefs;
|
||||
// Open up preferences with defined namespace
|
||||
prefs.begin("vulpes", false);
|
||||
|
||||
// Replace with your network credentials
|
||||
const char* ssid = WIFI_SSID;
|
||||
const char* password = WIFI_PASSWORD;
|
||||
|
||||
// Set web server port number to 80
|
||||
WiFiServer server(80);
|
||||
|
||||
// Variable to store the HTTP request
|
||||
String header;
|
||||
|
||||
// Auxiliar variables to store the current output state
|
||||
String output26State = "off";
|
||||
String output27State = "off";
|
||||
|
||||
// Assign output variables to GPIO pins
|
||||
const int output26 = 26;
|
||||
const int output27 = 27;
|
||||
|
||||
// Current time
|
||||
unsigned long currentTime = millis();
|
||||
// Previous time
|
||||
unsigned long previousTime = 0;
|
||||
// Define timeout time in milliseconds (example: 2000ms = 2s)
|
||||
const long timeoutTime = 2000;
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
// Initialize the output variables as outputs
|
||||
pinMode(output26, OUTPUT);
|
||||
pinMode(output27, OUTPUT);
|
||||
// Set outputs to LOW
|
||||
digitalWrite(output26, LOW);
|
||||
digitalWrite(output27, LOW);
|
||||
|
||||
preferences.putString("ssid", ssid);
|
||||
preferences.putString("password", password);
|
||||
|
||||
Serial.println("Network Credentials Saved using Preferences");
|
||||
|
||||
preferences.end();
|
||||
|
||||
// Connect to Wi-Fi network with SSID and password
|
||||
Serial.print("Connecting to ");
|
||||
Serial.println(ssid);
|
||||
WiFi.begin(ssid, password);
|
||||
while (WiFi.status() != WL_CONNECTED) {
|
||||
delay(500);
|
||||
Serial.print(".");
|
||||
}
|
||||
// Print local IP address and start web server
|
||||
Serial.println("");
|
||||
Serial.println("WiFi connected.");
|
||||
Serial.println("IP address: ");
|
||||
Serial.println(WiFi.localIP());
|
||||
server.begin();
|
||||
}
|
||||
|
||||
void loop(){
|
||||
WiFiClient client = server.available(); // Listen for incoming clients
|
||||
|
||||
if (client) { // If a new client connects,
|
||||
currentTime = millis();
|
||||
previousTime = currentTime;
|
||||
Serial.println("New Client."); // print a message out in the serial port
|
||||
String currentLine = ""; // make a String to hold incoming data from the client
|
||||
while (client.connected() && currentTime - previousTime <= timeoutTime) { // loop while the client's connected
|
||||
currentTime = millis();
|
||||
if (client.available()) { // if there's bytes to read from the client,
|
||||
char c = client.read(); // read a byte, then
|
||||
Serial.write(c); // print it out the serial monitor
|
||||
header += c;
|
||||
if (c == '\n') { // if the byte is a newline character
|
||||
// if the current line is blank, you got two newline characters in a row.
|
||||
// that's the end of the client HTTP request, so send a response:
|
||||
if (currentLine.length() == 0) {
|
||||
// HTTP headers always start with a response code (e.g. HTTP/1.1 200 OK)
|
||||
// and a content-type so the client knows what's coming, then a blank line:
|
||||
client.println("HTTP/1.1 200 OK");
|
||||
client.println("Content-type:text/html");
|
||||
client.println("Connection: close");
|
||||
client.println();
|
||||
|
||||
// turns the GPIOs on and off
|
||||
if (header.indexOf("GET /26/on") >= 0) {
|
||||
Serial.println("GPIO 26 on");
|
||||
output26State = "on";
|
||||
digitalWrite(output26, HIGH);
|
||||
} else if (header.indexOf("GET /26/off") >= 0) {
|
||||
Serial.println("GPIO 26 off");
|
||||
output26State = "off";
|
||||
digitalWrite(output26, LOW);
|
||||
} else if (header.indexOf("GET /27/on") >= 0) {
|
||||
Serial.println("GPIO 27 on");
|
||||
output27State = "on";
|
||||
digitalWrite(output27, HIGH);
|
||||
} else if (header.indexOf("GET /27/off") >= 0) {
|
||||
Serial.println("GPIO 27 off");
|
||||
output27State = "off";
|
||||
digitalWrite(output27, LOW);
|
||||
}
|
||||
|
||||
// Display the HTML web page
|
||||
client.println("<!DOCTYPE html><html>");
|
||||
client.println("<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
|
||||
client.println("<link rel=\"icon\" href=\"data:,\">");
|
||||
// CSS to style the on/off buttons
|
||||
// Feel free to change the background-color and font-size attributes to fit your preferences
|
||||
client.println("<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}");
|
||||
client.println(".button { background-color: #4CAF50; border: none; color: white; padding: 16px 40px;");
|
||||
client.println("text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;}");
|
||||
client.println(".button2 {background-color: #555555;}</style></head>");
|
||||
|
||||
// Web Page Heading
|
||||
client.println("<body><h1>ESP32 Web Server</h1>");
|
||||
|
||||
// Display current state, and ON/OFF buttons for GPIO 26
|
||||
client.println("<p>GPIO 26 - State " + output26State + "</p>");
|
||||
// If the output26State is off, it displays the ON button
|
||||
if (output26State=="off") {
|
||||
client.println("<p><a href=\"/26/on\"><button class=\"button\">ON</button></a></p>");
|
||||
} else {
|
||||
client.println("<p><a href=\"/26/off\"><button class=\"button button2\">OFF</button></a></p>");
|
||||
}
|
||||
|
||||
// Display current state, and ON/OFF buttons for GPIO 27
|
||||
client.println("<p>GPIO 27 - State " + output27State + "</p>");
|
||||
// If the output27State is off, it displays the ON button
|
||||
if (output27State=="off") {
|
||||
client.println("<p><a href=\"/27/on\"><button class=\"button\">ON</button></a></p>");
|
||||
} else {
|
||||
client.println("<p><a href=\"/27/off\"><button class=\"button button2\">OFF</button></a></p>");
|
||||
}
|
||||
client.println("</body></html>");
|
||||
|
||||
// The HTTP response ends with another blank line
|
||||
client.println();
|
||||
// Break out of the while loop
|
||||
break;
|
||||
} else { // if you got a newline, then clear currentLine
|
||||
currentLine = "";
|
||||
}
|
||||
} else if (c != '\r') { // if you got anything else but a carriage return character,
|
||||
currentLine += c; // add it to the end of the currentLine
|
||||
}
|
||||
}
|
||||
}
|
||||
// Clear the header variable
|
||||
header = "";
|
||||
// Close the connection
|
||||
client.stop();
|
||||
Serial.println("Client disconnected.");
|
||||
Serial.println("");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/*********
|
||||
Rui Santos
|
||||
Complete project details at https://randomnerdtutorials.com
|
||||
*********/
|
||||
|
||||
// include wifi password
|
||||
#include "config.h"
|
||||
// Load Wi-Fi library
|
||||
#include <WiFi.h>
|
||||
|
||||
// Replace with your network credentials
|
||||
const char* ssid = WIFI_SSID;
|
||||
const char* password = WIFI_PASSWORD;
|
||||
|
||||
// Set web server port number to 80
|
||||
WiFiServer server(80);
|
||||
|
||||
// Variable to store the HTTP request
|
||||
String header;
|
||||
|
||||
// Auxiliar variables to store the current output state
|
||||
String output26State = "off";
|
||||
String output27State = "off";
|
||||
|
||||
// Assign output variables to GPIO pins
|
||||
const int output26 = 26;
|
||||
const int output27 = 27;
|
||||
|
||||
// Current time
|
||||
unsigned long currentTime = millis();
|
||||
// Previous time
|
||||
unsigned long previousTime = 0;
|
||||
// Define timeout time in milliseconds (example: 2000ms = 2s)
|
||||
const long timeoutTime = 2000;
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
// Initialize the output variables as outputs
|
||||
pinMode(output26, OUTPUT);
|
||||
pinMode(output27, OUTPUT);
|
||||
// Set outputs to LOW
|
||||
digitalWrite(output26, LOW);
|
||||
digitalWrite(output27, LOW);
|
||||
|
||||
// Connect to Wi-Fi network with SSID and password
|
||||
Serial.print("Connecting to ");
|
||||
Serial.println(ssid);
|
||||
WiFi.begin(ssid, password);
|
||||
while (WiFi.status() != WL_CONNECTED) {
|
||||
delay(500);
|
||||
Serial.print(".");
|
||||
}
|
||||
// Print local IP address and start web server
|
||||
Serial.println("");
|
||||
Serial.println("WiFi connected.");
|
||||
Serial.println("IP address: ");
|
||||
Serial.println(WiFi.localIP());
|
||||
server.begin();
|
||||
}
|
||||
|
||||
void loop(){
|
||||
WiFiClient client = server.available(); // Listen for incoming clients
|
||||
|
||||
if (client) { // If a new client connects,
|
||||
currentTime = millis();
|
||||
previousTime = currentTime;
|
||||
Serial.println("New Client."); // print a message out in the serial port
|
||||
String currentLine = ""; // make a String to hold incoming data from the client
|
||||
while (client.connected() && currentTime - previousTime <= timeoutTime) { // loop while the client's connected
|
||||
currentTime = millis();
|
||||
if (client.available()) { // if there's bytes to read from the client,
|
||||
char c = client.read(); // read a byte, then
|
||||
Serial.write(c); // print it out the serial monitor
|
||||
header += c;
|
||||
if (c == '\n') { // if the byte is a newline character
|
||||
// if the current line is blank, you got two newline characters in a row.
|
||||
// that's the end of the client HTTP request, so send a response:
|
||||
if (currentLine.length() == 0) {
|
||||
// HTTP headers always start with a response code (e.g. HTTP/1.1 200 OK)
|
||||
// and a content-type so the client knows what's coming, then a blank line:
|
||||
client.println("HTTP/1.1 200 OK");
|
||||
client.println("Content-type:text/html");
|
||||
client.println("Connection: close");
|
||||
client.println();
|
||||
|
||||
// turns the GPIOs on and off
|
||||
if (header.indexOf("GET /26/on") >= 0) {
|
||||
Serial.println("GPIO 26 on");
|
||||
output26State = "on";
|
||||
digitalWrite(output26, HIGH);
|
||||
} else if (header.indexOf("GET /26/off") >= 0) {
|
||||
Serial.println("GPIO 26 off");
|
||||
output26State = "off";
|
||||
digitalWrite(output26, LOW);
|
||||
} else if (header.indexOf("GET /27/on") >= 0) {
|
||||
Serial.println("GPIO 27 on");
|
||||
output27State = "on";
|
||||
digitalWrite(output27, HIGH);
|
||||
} else if (header.indexOf("GET /27/off") >= 0) {
|
||||
Serial.println("GPIO 27 off");
|
||||
output27State = "off";
|
||||
digitalWrite(output27, LOW);
|
||||
}
|
||||
|
||||
// Display the HTML web page
|
||||
client.println("<!DOCTYPE html><html>");
|
||||
client.println("<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
|
||||
client.println("<link rel=\"icon\" href=\"data:,\">");
|
||||
// CSS to style the on/off buttons
|
||||
// Feel free to change the background-color and font-size attributes to fit your preferences
|
||||
client.println("<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}");
|
||||
client.println(".button { background-color: #4CAF50; border: none; color: white; padding: 16px 40px;");
|
||||
client.println("text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;}");
|
||||
client.println(".button2 {background-color: #555555;}</style></head>");
|
||||
|
||||
// Web Page Heading
|
||||
client.println("<body><h1>ESP32 Web Server</h1>");
|
||||
|
||||
// Display current state, and ON/OFF buttons for GPIO 26
|
||||
client.println("<p>GPIO 26 - State " + output26State + "</p>");
|
||||
// If the output26State is off, it displays the ON button
|
||||
if (output26State=="off") {
|
||||
client.println("<p><a href=\"/26/on\"><button class=\"button\">ON</button></a></p>");
|
||||
} else {
|
||||
client.println("<p><a href=\"/26/off\"><button class=\"button button2\">OFF</button></a></p>");
|
||||
}
|
||||
|
||||
// Display current state, and ON/OFF buttons for GPIO 27
|
||||
client.println("<p>GPIO 27 - State " + output27State + "</p>");
|
||||
// If the output27State is off, it displays the ON button
|
||||
if (output27State=="off") {
|
||||
client.println("<p><a href=\"/27/on\"><button class=\"button\">ON</button></a></p>");
|
||||
} else {
|
||||
client.println("<p><a href=\"/27/off\"><button class=\"button button2\">OFF</button></a></p>");
|
||||
}
|
||||
client.println("</body></html>");
|
||||
|
||||
// The HTTP response ends with another blank line
|
||||
client.println();
|
||||
// Break out of the while loop
|
||||
break;
|
||||
} else { // if you got a newline, then clear currentLine
|
||||
currentLine = "";
|
||||
}
|
||||
} else if (c != '\r') { // if you got anything else but a carriage return character,
|
||||
currentLine += c; // add it to the end of the currentLine
|
||||
}
|
||||
}
|
||||
}
|
||||
// Clear the header variable
|
||||
header = "";
|
||||
// Close the connection
|
||||
client.stop();
|
||||
Serial.println("Client disconnected.");
|
||||
Serial.println("");
|
||||
}
|
||||
}
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
import sqlite3
|
||||
|
||||
connection = sqlite3.connect('database.db')
|
||||
|
||||
with open('schema.sql') as f:
|
||||
connection.executescript(f.read())
|
||||
|
||||
cur = connection.cursor()
|
||||
|
||||
# cur.execute("INSERT INTO posts (title, content) VALUES (?, ?)",
|
||||
# ('First Post', 'Content for the first post')
|
||||
# )
|
||||
#
|
||||
# cur.execute("INSERT INTO posts (title, content) VALUES (?, ?)",
|
||||
# ('Second Post', 'Content for the second post')
|
||||
# )
|
||||
|
||||
connection.commit()
|
||||
connection.close()
|
||||
-116
@@ -1,116 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sqlite3
|
||||
import aprs
|
||||
import json
|
||||
import aprslib
|
||||
import configparser
|
||||
import time
|
||||
import logging
|
||||
|
||||
def read_config():
|
||||
config = configparser.ConfigParser()
|
||||
config.read('config.ini')
|
||||
return config
|
||||
|
||||
def get_db_connection():
|
||||
conn = sqlite3.connect('database.db')
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
def main():
|
||||
|
||||
# Add the call and location of this station to the packet info
|
||||
config = read_config()
|
||||
# MYCALL = os.environ.get("MYCALL", "W1CDN")
|
||||
# KISS_HOST = os.environ.get("KISS_HOST", "192.168.0.30")
|
||||
# KISS_PORT = os.environ.get("KISS_PORT", "8001")
|
||||
|
||||
logging.basicConfig(filename=config['Settings']['log_path'], level=logging.DEBUG, \
|
||||
format='%(asctime)s - %(message)s')
|
||||
logging.debug('kiss_and_db.py running')
|
||||
|
||||
ki = aprs.TCPKISS(host=config['Settings']['kiss_host'], port=int(config['Settings']['kiss_port']))
|
||||
ki.start()
|
||||
|
||||
|
||||
# Make a simple frame and send it
|
||||
frame = aprs.APRSFrame.ui(
|
||||
destination="APZ001",
|
||||
source=config['Settings']['mycall'],
|
||||
path=["WIDE1-1"],
|
||||
info=b">Hello World!",
|
||||
)
|
||||
#ki.write(frame)
|
||||
|
||||
# Watch for new packets to come in
|
||||
while True:
|
||||
conn = get_db_connection()
|
||||
for frame in ki.read(min_frames=1):
|
||||
try:
|
||||
a = aprslib.parse(str(frame))
|
||||
a['station_call'] = config['Settings']['station_call']
|
||||
a['station_lat'] = config['Settings']['station_lat']
|
||||
a['station_lon'] = config['Settings']['station_lon']
|
||||
a['created_unix'] = int(time.time())
|
||||
print(a)
|
||||
# Make this a string and deal with it later (probably a mistake)
|
||||
a['path'] = str(a['path'])
|
||||
# Store true/false as 1/0
|
||||
if 'alive' in a:
|
||||
if a['alive'] == True:
|
||||
a['alive'] = 1
|
||||
else:
|
||||
a['alive'] = 0
|
||||
# Build an INSERT statement based on the fields we have from the frame
|
||||
attrib_names = ', '.join('"%s"' % w for w in a.keys())
|
||||
attrib_values = ", ".join("?" * len(a.keys()))
|
||||
|
||||
|
||||
try:
|
||||
# Insert data
|
||||
sql = "INSERT INTO frames ("+attrib_names+") VALUES ("+attrib_values+")"
|
||||
logging.debug(sql)
|
||||
conn.execute(sql, list(a.values()))
|
||||
|
||||
# TODO update stations table here
|
||||
# Original intent was to include the id from the frames table,
|
||||
# but that would mean making another query.
|
||||
# It's not immediately needed, so I'm skipping it.
|
||||
# Build query
|
||||
# "from" is wrappedin [] because it is a reserved word and using '' doesn't work.
|
||||
# https://www.sqlite.org/lang_keywords.html
|
||||
#try:
|
||||
station_update = "'"+a['from'] +"', '"+ str(a['created_unix']) +"', '1'"
|
||||
query3 = "INSERT INTO stations ([from], last_heard_unix, count) \
|
||||
VALUES("+station_update+") \
|
||||
ON CONFLICT([from]) \
|
||||
DO UPDATE SET count = count + 1,\
|
||||
last_heard_unix = excluded.last_heard_unix;"
|
||||
#print(query3)
|
||||
logging.debug(query3)
|
||||
# Insert/update data
|
||||
conn.execute(query3)
|
||||
|
||||
conn.commit()
|
||||
#except:
|
||||
# print("Stations table couldn't be updated.")
|
||||
|
||||
# TODO remove packets that are older ('created') than a limit set in config.ini
|
||||
# "5 minutes" also works
|
||||
#conn.execute("DELETE FROM frames WHERE created < DATETIME('now', '"+config['Settings']['keep_time']+"')")
|
||||
#conn.commit()
|
||||
except:
|
||||
#print("Error with SQLite!")
|
||||
logging.error("Error with SQLite!")
|
||||
except:
|
||||
#print("Frame could not be parsed.")
|
||||
logging.error("Frame could not be parsed.")
|
||||
|
||||
|
||||
conn.close()
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,8 +0,0 @@
|
||||
flask
|
||||
flask_restful
|
||||
aprs3
|
||||
kiss3
|
||||
kiss
|
||||
aprslib
|
||||
sqlite3
|
||||
json
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
DROP TABLE IF EXISTS frames;
|
||||
|
||||
CREATE TABLE frames (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
addresse TEXT,
|
||||
alive INT,
|
||||
altitude REAL,
|
||||
comment TEXT,
|
||||
course REAL,
|
||||
created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
created_unix INT,
|
||||
format TEXT,
|
||||
frame TEXT,
|
||||
"from" TEXT,
|
||||
gpsfixstatus TEXT,
|
||||
latitude REAL,
|
||||
longitude REAL,
|
||||
mbits INT,
|
||||
messagecapable INT,
|
||||
message_text TEXT,
|
||||
msgNo INT,
|
||||
mtype TEXT,
|
||||
object_format TEXT,
|
||||
object_name TEXT,
|
||||
path TEXT,
|
||||
phg REAL,
|
||||
phg_dir TEXT,
|
||||
phg_gain REAL,
|
||||
phg_height REAL,
|
||||
phg_power REAL,
|
||||
phg_range REAL,
|
||||
posambiguity INT,
|
||||
raw TEXT,
|
||||
raw_timestamp TEXT,
|
||||
speed REAL,
|
||||
station_call TEXT,
|
||||
station_lat REAL,
|
||||
station_lon REAL,
|
||||
status TEXT,
|
||||
subpacket TEXT,
|
||||
symbol TEXT,
|
||||
symbol_table TEXT,
|
||||
telemetry TEXT,
|
||||
timestamp INT,
|
||||
"to" TEXT,
|
||||
tEQNS TEXT,
|
||||
tPARM TEXT,
|
||||
tUNIT TEXT,
|
||||
via TEXT,
|
||||
weather TEXT,
|
||||
wx_raw_timestamp TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE "stations" (
|
||||
"id" INTEGER NOT NULL UNIQUE,
|
||||
"from" TEXT UNIQUE,
|
||||
"frames_id" INTEGER,
|
||||
"last_heard_unix" INTEGER,
|
||||
"count" INTEGER,
|
||||
PRIMARY KEY("id" AUTOINCREMENT)
|
||||
);
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Run `chmod +x start-aprs_api.sh` so this can be run
|
||||
|
||||
screen -dmS aprs_api python3 /home/pi/aprs_tool/api_waitress.py
|
||||
@@ -1,59 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{station_call}} Status</title>
|
||||
|
||||
<style>
|
||||
table, th, td {
|
||||
border: 1px solid black;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{station_call}} Status</h1>
|
||||
{{station_lat}}, {{station_lon}}
|
||||
|
||||
<h2> Recent RF Packets </h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th> from </th>
|
||||
<th> object_name </th>
|
||||
<th> raw </th>
|
||||
<th> created (utc) </th>
|
||||
<th> more </th>
|
||||
</tr>
|
||||
{% for i in frames %}
|
||||
<tr>
|
||||
<td> <a href="https://digi.w1cdn.net/aprs_api/packets?from={{ i['from'] }}">{{ i['from'] }}</a> </td>
|
||||
<td> {{ i['object_name'] }} </td>
|
||||
<td> {{ i['raw'] }} </td>
|
||||
<td> {{ i['created'] }} </td>
|
||||
<td> <a href="https://digi.w1cdn.net/aprs_api/packets?id={{ i['id'] }}">query</a>,
|
||||
<a href="https://aprs.fi/#!mt=roadmap&z=12&call=a%2F{{ i['from'] }}">aprs.fi</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
<h2> Recent Stations </h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th> from </th>
|
||||
<th> last heard (utc) </th>
|
||||
<th> count </th>
|
||||
<th> more </th>
|
||||
</tr>
|
||||
{% for i in stations %}
|
||||
<tr>
|
||||
<td> <a href="https://digi.w1cdn.net/aprs_api/packets?from={{ i['from'] }}">{{ i['from'] }}</a> </td>
|
||||
<td> {{ i['last_heard'] }} </td>
|
||||
<td> {{ i['count']}} </td>
|
||||
<td> <a href="https://aprs.fi/#!mt=roadmap&z=12&call=a%2F{{ i['from'] }}">aprs.fi</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
<h2> Help </h2>
|
||||
This is a work in progress. See <a href="https://amiok.net/gitea/W1CDN/aprs_tool">https://amiok.net/gitea/W1CDN/aprs_tool</a> for usage.
|
||||
</body>
|
||||
</html>
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
|
||||
# Learn how to update database
|
||||
|
||||
import sqlite3
|
||||
|
||||
def get_db_connection():
|
||||
conn = sqlite3.connect('database.db')
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
conn = get_db_connection()
|
||||
|
||||
# Grab a random row from frames table and pretend it is new
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT [from], id, created_unix FROM frames ORDER BY RANDOM() LIMIT 1;")
|
||||
rows = cur.fetchall()
|
||||
results = dict(rows[0])
|
||||
values = ', '.join('"%s"' % w for w in results.values())
|
||||
|
||||
|
||||
# Build query
|
||||
# "from" is wrappedin [] because it is a reserved word and using '' doesn't work.
|
||||
query3 = "INSERT INTO stations ([from], frames_id, last_heard_unix, count) \
|
||||
VALUES("+values+", 1) \
|
||||
ON CONFLICT([from]) \
|
||||
DO UPDATE SET count = count + 1;"
|
||||
|
||||
# example https://stackoverflow.com/a/50718957/2152245
|
||||
# query2 = "INSERT INTO stations ([from], frames_id, last_heard_unix, count) \
|
||||
# VALUES('KC9TZN-8', 4068, 1687623864, 1) \
|
||||
# ON CONFLICT([from]) \
|
||||
# DO UPDATE SET count = count + 1;"
|
||||
|
||||
conn.execute(query3)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
.pio
|
||||
.vscode/.browse.c_cpp.db*
|
||||
.vscode/c_cpp_properties.json
|
||||
.vscode/launch.json
|
||||
.vscode/ipch
|
||||
.vscode/*
|
||||
*/config.h
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
{
|
||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"platformio.platformio-ide"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
"ms-vscode.cpptools-extension-pack"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
|
||||
This directory is intended for project header files.
|
||||
|
||||
A header file is a file containing C declarations and macro definitions
|
||||
to be shared between several project source files. You request the use of a
|
||||
header file in your project source file (C, C++, etc) located in `src` folder
|
||||
by including it, with the C preprocessing directive `#include'.
|
||||
|
||||
```src/main.c
|
||||
|
||||
#include "header.h"
|
||||
|
||||
int main (void)
|
||||
{
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Including a header file produces the same results as copying the header file
|
||||
into each source file that needs it. Such copying would be time-consuming
|
||||
and error-prone. With a header file, the related declarations appear
|
||||
in only one place. If they need to be changed, they can be changed in one
|
||||
place, and programs that include the header file will automatically use the
|
||||
new version when next recompiled. The header file eliminates the labor of
|
||||
finding and changing all the copies as well as the risk that a failure to
|
||||
find one copy will result in inconsistencies within a program.
|
||||
|
||||
In C, the usual convention is to give header files names that end with `.h'.
|
||||
It is most portable to use only letters, digits, dashes, and underscores in
|
||||
header file names, and at most one dot.
|
||||
|
||||
Read more about using header files in official GCC documentation:
|
||||
|
||||
* Include Syntax
|
||||
* Include Operation
|
||||
* Once-Only Headers
|
||||
* Computed Includes
|
||||
|
||||
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
|
||||
@@ -0,0 +1,46 @@
|
||||
|
||||
This directory is intended for project specific (private) libraries.
|
||||
PlatformIO will compile them to static libraries and link into executable file.
|
||||
|
||||
The source code of each library should be placed in a an own separate directory
|
||||
("lib/your_library_name/[here are source files]").
|
||||
|
||||
For example, see a structure of the following two libraries `Foo` and `Bar`:
|
||||
|
||||
|--lib
|
||||
| |
|
||||
| |--Bar
|
||||
| | |--docs
|
||||
| | |--examples
|
||||
| | |--src
|
||||
| | |- Bar.c
|
||||
| | |- Bar.h
|
||||
| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
|
||||
| |
|
||||
| |--Foo
|
||||
| | |- Foo.c
|
||||
| | |- Foo.h
|
||||
| |
|
||||
| |- README --> THIS FILE
|
||||
|
|
||||
|- platformio.ini
|
||||
|--src
|
||||
|- main.c
|
||||
|
||||
and a contents of `src/main.c`:
|
||||
```
|
||||
#include <Foo.h>
|
||||
#include <Bar.h>
|
||||
|
||||
int main (void)
|
||||
{
|
||||
...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
PlatformIO Library Dependency Finder will find automatically dependent
|
||||
libraries scanning project source files.
|
||||
|
||||
More information about PlatformIO Library Dependency Finder
|
||||
- https://docs.platformio.org/page/librarymanager/ldf.html
|
||||
@@ -0,0 +1,26 @@
|
||||
; PlatformIO Project Configuration File
|
||||
;
|
||||
; Build options: build flags, source filter
|
||||
; Upload options: custom upload port, speed and extra flags
|
||||
; Library options: dependencies, extra library storages
|
||||
; Advanced options: extra scripting
|
||||
;
|
||||
; Please visit documentation for the other options and examples
|
||||
; https://docs.platformio.org/page/projectconf.html
|
||||
|
||||
[env:esp32doit-devkit-v1]
|
||||
platform = espressif32
|
||||
board = esp32doit-devkit-v1
|
||||
framework = arduino
|
||||
upload_speed = 921600
|
||||
monitor_speed = 115200
|
||||
monitor_filters = esp32_exception_decoder
|
||||
build_type = debug
|
||||
lib_deps =
|
||||
me-no-dev/AsyncTCP@^1.1.1
|
||||
me-no-dev/ESP Async WebServer@^1.2.3
|
||||
contrem/arduino-timer@^3.0.1
|
||||
jandelgado/JLed@^4.13.1
|
||||
https://github.com/adafruit/RTClib.git
|
||||
adafruit/Adafruit BusIO@^1.14.3
|
||||
erropix/ESP32 AnalogWrite@^0.2
|
||||
@@ -0,0 +1,2 @@
|
||||
#define WIFI_SSID "wifi_name"
|
||||
#define WIFI_PASSWORD "wifi_pass"
|
||||
@@ -0,0 +1,631 @@
|
||||
/*********
|
||||
Rui Santos
|
||||
Complete project details at https://RandomNerdTutorials.com/esp32-esp8266-input-data-html-form/
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files.
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
*********/
|
||||
|
||||
// include wifi password
|
||||
#include "config.h"
|
||||
#include <Arduino.h>
|
||||
#include <WiFi.h>
|
||||
#include <AsyncTCP.h>
|
||||
#include <SPIFFS.h>
|
||||
#include <Preferences.h>
|
||||
#include "morse.h"
|
||||
#include <Adafruit_BusIO_Register.h> // for DS3231
|
||||
#include <RTClib.h> // for DS3231
|
||||
#include <string>
|
||||
|
||||
// download zip from https://github.com/me-no-dev/ESPAsyncWebServer and install.
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
AsyncWebServer server(80);
|
||||
|
||||
// Assign output variables to GPIO pins
|
||||
//LED_BUILTIN for ESP32 onboard LED, 32 for transmitter keyer
|
||||
const int keyer = 32;
|
||||
const int blinker = LED_BUILTIN;
|
||||
|
||||
RTC_DS3231 rtc; // set up RTC
|
||||
const int alarmPin = 4; // pin to monitor for RTC alarms
|
||||
|
||||
// Read from config.h
|
||||
const char* ssid = WIFI_SSID;
|
||||
const char* password = WIFI_PASSWORD;
|
||||
|
||||
const char* PARAM_SEND = "inputSend";
|
||||
const char* PARAM_WPM = "inputWPM";
|
||||
const char* PARAM_MSG = "inputMsg";
|
||||
const char* PARAM_CMSG = "inputCustomMsg";
|
||||
const char* PARAM_FLOAT = "inputFloat";
|
||||
const char* PARAM_TIME = "inputTimeUnix";
|
||||
const char* PARAM_START = "inputStartTimeUnix";
|
||||
const char* PARAM_RUNNING = "programRunning";
|
||||
const char* PARAM_STEPLENGTH = "inputStepLength";
|
||||
const char* PARAM_CYCLEID = "inputCycleID";
|
||||
const char* PARAM_NTRANS = "inputNtransmitters";
|
||||
|
||||
// Global variables
|
||||
int yourInputSend;
|
||||
int yourInputWPM;
|
||||
int yourInputMsg;
|
||||
int yourInputMsg_old; // to save previous state and check changes
|
||||
String yourInputCustomMsg;
|
||||
float yourInputFloat;
|
||||
uint32_t yourInputTime; //to keep time
|
||||
uint32_t yourInputStartTimeUnix;
|
||||
bool startProgram;
|
||||
bool programRunning;
|
||||
int yourInputStepLength;
|
||||
int yourInputCycleID;
|
||||
int yourInputNtransmitters;
|
||||
long start_millis = 0;
|
||||
long stop_millis = 0;
|
||||
long pause_until_millis = 0;
|
||||
|
||||
// HTML web page to handle input fields
|
||||
const char index_html[] PROGMEM = R"rawliteral(
|
||||
<!DOCTYPE HTML><html><head>
|
||||
<link rel="icon" href="data:,">
|
||||
<title>Vulpes Radio Orienteering Controller</title>
|
||||
<style>
|
||||
.inv_message {
|
||||
display: none;
|
||||
}
|
||||
.inv_program {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script type="text/javascript">
|
||||
// Utility from https://webreflection.medium.com/using-the-input-datetime-local-9503e7efdce
|
||||
Date.prototype.toDatetimeLocal = function toDatetimeLocal() {
|
||||
var
|
||||
date = this,
|
||||
ten = function (i) {
|
||||
return (i < 10 ? '0' : '') + i;
|
||||
},
|
||||
YYYY = date.getFullYear(),
|
||||
MM = ten(date.getMonth() + 1),
|
||||
DD = ten(date.getDate()),
|
||||
HH = ten(date.getHours()),
|
||||
II = ten(date.getMinutes()),
|
||||
SS = ten(date.getSeconds())
|
||||
;
|
||||
return YYYY + '-' + MM + '-' + DD + 'T' +
|
||||
HH + ':' + II + ':' + SS;
|
||||
}
|
||||
|
||||
// Submit timestamps as unix seconds when form is submitted
|
||||
var putDate = function(form) {
|
||||
form.inputTimeUnix.value = Math.floor(Date.now() / 1000);// - new Date().getTimezoneOffset()*60;
|
||||
form.inputStartTimeUnix.value = ((Date.parse(js_start_time_unix_entry.value))/1000);
|
||||
//document.getElementById("js_start_time_unix").value = ((Date.parse(js_start_time_unix_entry.value))/1000);
|
||||
}
|
||||
// Fill in page values
|
||||
window.onload = function() {
|
||||
s = %inputStartTimeUnix%;
|
||||
current_start = new Date(s * 1000);
|
||||
document.getElementById('current-start').innerHTML = current_start.toLocaleString();
|
||||
// Show the local time as a string
|
||||
local_time_unix = new Date().toLocaleString();//toUTCString();
|
||||
document.getElementById('local-time-unix').innerHTML = local_time_unix.toString();
|
||||
// Fill in the start time field as local time
|
||||
document.getElementById('js_start_time_unix_entry').value = current_start.toDatetimeLocal();
|
||||
|
||||
// Fill in the other form fields
|
||||
document.getElementById("send-program").value = %inputSend%;
|
||||
document.getElementById("message").value = %inputMsg%;
|
||||
}
|
||||
|
||||
</script></head><body>
|
||||
<h1>Vulpes Radio Orienteering Controller</h1>
|
||||
<p>Local time: <b><span id=local-time-unix></span></b></p>
|
||||
|
||||
<form action="/get" onsubmit="putDate(this);" accept-charset=utf-8>
|
||||
<h2>General Settings</h2>
|
||||
<p>Sending program:
|
||||
<select name="inputSend" id="send-program">
|
||||
<option value="0">0 - Off</option>
|
||||
<option value="1">1 - Continuous</option>
|
||||
<option value="2">2 - Cycle</option>
|
||||
</select><br>
|
||||
|
||||
Message:
|
||||
<select name="inputMsg" id="message">
|
||||
<option value="0">0 - Custom Message</option>
|
||||
<option value="1">1 - MOE</option>
|
||||
<option value="2">2 - MOI</option>
|
||||
<option value="3">3 - MOS</option>
|
||||
<option value="4">4 - MOH</option>
|
||||
<option value="5">5 - MO5</option>
|
||||
</select><br>
|
||||
|
||||
<!-- Hidden unless "0 - Custom Message" is selected -->
|
||||
<span id="message0" class="inv_message">
|
||||
Custom message: <input type="text" name="inputCustomMsg" value = "%inputCustomMsg%"><br>
|
||||
</span>
|
||||
|
||||
Speed: <input type="number" name="inputWPM" value = %inputWPM%> WPM
|
||||
</p>
|
||||
|
||||
<!-- Hidden unless "2 - Cycle" is selected -->
|
||||
<span id="program2" class="inv_program">
|
||||
<h2>Cycle Settings</h2>
|
||||
<p>Only applies when <em>Sending Program</em> is set to "2 - Cycle". You cannot set a cycle start date more than a month in advance.<br>
|
||||
Cycle start time <input type="datetime-local" id="js_start_time_unix_entry" /><br>
|
||||
Current value: <b><span id=current-start></span></b><br>
|
||||
|
||||
<!-- JS converts the entered start time to a unix timestamp, and copies that value
|
||||
to this hidden field so the user doesn't have to see it. -->
|
||||
<input type="hidden" name="inputStartTimeUnix" id="js_start_time_unix" /></p>
|
||||
Step length: <input type="number" name="inputStepLength" min=1000 step=1000 value = %inputStepLength%> milliseconds <br>
|
||||
Cycle ID: <input type="number" name="inputCycleID" min=1 value = %inputCycleID%><br>
|
||||
Number of transmitters: <input type="number" name="inputNtransmitters" min=1 value = %inputNtransmitters%><br>
|
||||
</p>
|
||||
</span>
|
||||
|
||||
<!-- This field is hidden so people don't change the submit time (it will be wrong).
|
||||
The value is automatically filled in with JS. -->
|
||||
<input type="hidden" name="inputTimeUnix" id="js_time_unix">
|
||||
|
||||
<!-- Extra fields just in case I need them -->
|
||||
<input type="hidden" name="inputFloat" value = %inputFloat%>
|
||||
|
||||
<input type="submit" value="Submit"">
|
||||
</form>
|
||||
|
||||
|
||||
|
||||
<iframe style="display:none" name="hidden-form" id="hidden-form"></iframe>
|
||||
<script type="text/javascript">
|
||||
// Show more stuff depending on selected values
|
||||
// https://stackoverflow.com/a/24849350
|
||||
show_message = function () {
|
||||
'use strict';
|
||||
var vis_message = document.querySelector('.vis_message'),
|
||||
target = document.getElementById("message"+this.value);
|
||||
if (vis_message !== null) {
|
||||
vis_message.className = 'inv_message';
|
||||
}
|
||||
if (target !== null ) {
|
||||
target.className = 'vis_message';
|
||||
}
|
||||
}
|
||||
show_program = function () {
|
||||
'use strict';
|
||||
var vis_program = document.querySelector('.vis_program'),
|
||||
target = document.getElementById("program"+this.value);
|
||||
if (vis_program !== null) {
|
||||
vis_program.className = 'inv_program';
|
||||
}
|
||||
if (target !== null ) {
|
||||
target.className = 'vis_program';
|
||||
}
|
||||
}
|
||||
document
|
||||
.getElementById('message')
|
||||
.addEventListener('change', show_message);
|
||||
document
|
||||
.getElementById('send-program')
|
||||
.addEventListener('change', show_program);
|
||||
</script>
|
||||
</body></html>)rawliteral";
|
||||
|
||||
|
||||
void notFound(AsyncWebServerRequest *request) {
|
||||
request->send(404, "text/plain", "Not found");
|
||||
}
|
||||
|
||||
String readFile(fs::FS &fs, const char * path){
|
||||
//Serial.printf("Reading file: %s\r\n", path);
|
||||
File file = fs.open(path, "r");
|
||||
if(!file || file.isDirectory()){
|
||||
Serial.println("- empty file or failed to open file");
|
||||
return String();
|
||||
}
|
||||
//Serial.println("- read from file:");
|
||||
String fileContent;
|
||||
while(file.available()){
|
||||
fileContent+=String((char)file.read());
|
||||
}
|
||||
file.close();
|
||||
//Serial.println(fileContent);
|
||||
return fileContent;
|
||||
}
|
||||
|
||||
void writeFile(fs::FS &fs, const char * path, const char * message){
|
||||
Serial.printf("Writing file: %s\r\n", path);
|
||||
File file = fs.open(path, "w");
|
||||
if(!file){
|
||||
Serial.println("- failed to open file for writing");
|
||||
return;
|
||||
}
|
||||
if(file.print(message)){
|
||||
Serial.println("- file written");
|
||||
} else {
|
||||
Serial.println("- write failed");
|
||||
}
|
||||
file.close();
|
||||
}
|
||||
|
||||
// Replaces placeholder in web UI with stored values
|
||||
String processor(const String& var){
|
||||
//Serial.println(var);
|
||||
if(var == "inputCustomMsg"){
|
||||
return readFile(SPIFFS, "/inputCustomMsg.txt");
|
||||
}
|
||||
else if(var == "inputSend"){
|
||||
return readFile(SPIFFS, "/inputSend.txt");
|
||||
}
|
||||
else if(var == "inputWPM"){
|
||||
return readFile(SPIFFS, "/inputWPM.txt");
|
||||
}
|
||||
else if(var == "inputMsg"){
|
||||
return readFile(SPIFFS, "/inputMsg.txt");
|
||||
}
|
||||
else if(var == "inputStepLength"){
|
||||
return readFile(SPIFFS, "/inputStepLength.txt");
|
||||
}
|
||||
else if(var == "inputCycleID"){
|
||||
return readFile(SPIFFS, "/inputCycleID.txt");
|
||||
}
|
||||
else if(var == "inputNtransmitters"){
|
||||
return readFile(SPIFFS, "/inputNtransmitters.txt");
|
||||
}
|
||||
else if(var == "inputFloat"){
|
||||
return readFile(SPIFFS, "/inputFloat.txt");
|
||||
} else if(var == "inputStartTimeUnix"){
|
||||
// Webform breaks if this value is empty.
|
||||
String temp = readFile(SPIFFS, "/inputStartTimeUnix.txt");
|
||||
if(temp == ""){
|
||||
temp = "0";
|
||||
}
|
||||
return temp;
|
||||
}
|
||||
return String();
|
||||
}
|
||||
|
||||
|
||||
// Set up arduinomorse pin and default WPM
|
||||
LEDMorseSender sender_blink(blinker, 10.0f); //f makes it a float
|
||||
LEDMorseSender sender_key(keyer, 10.0f);
|
||||
|
||||
//================================================================================
|
||||
// setup(): stuff that only gets done once, after power up (KB1OIQ's description)
|
||||
//================================================================================
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
// Get arduinomorse ready to go
|
||||
sender_blink.setup();
|
||||
sender_key.setup();
|
||||
|
||||
pinMode(alarmPin, INPUT_PULLUP); // Set alarm pin as pullup
|
||||
|
||||
if (! rtc.begin()) {
|
||||
Serial.println("Couldn't find RTC");
|
||||
Serial.flush();
|
||||
while (1) delay(10);
|
||||
}
|
||||
|
||||
if (rtc.lostPower()) {
|
||||
Serial.println("RTC lost power, let's set the time!");
|
||||
// When time needs to be set on a new device, or after a power loss, the
|
||||
// following line sets the RTC to the date & time this sketch was compiled
|
||||
rtc.adjust(DateTime(__DATE__, __TIME__));
|
||||
// This line sets the RTC with an explicit date & time, for example to set
|
||||
// January 21, 2014 at 3am you would call:
|
||||
//rtc.adjust(DateTime(2023, 9, 2, 17, 32, 0));
|
||||
}
|
||||
|
||||
// Report the RTC time
|
||||
Serial.print("RTC time on startup: ");
|
||||
Serial.println(rtc.now().unixtime());
|
||||
Serial.println(rtc.now().timestamp());
|
||||
|
||||
// Are there any RTC alarms set?
|
||||
DateTime alarm_one = rtc.getAlarm1(); // Get the current time
|
||||
char buff[] = "Alarm 1 set for at hh:mm:ss DDD, DD MMM YYYY";
|
||||
Serial.print(alarm_one.toString(buff));
|
||||
Serial.println(" (only HH:MM:SS day-of-month are accurate)");
|
||||
|
||||
// Initialize the output variables as outputs
|
||||
pinMode(keyer, OUTPUT);
|
||||
pinMode(blinker, OUTPUT);
|
||||
// Set outputs to LOW
|
||||
digitalWrite(keyer, LOW);
|
||||
digitalWrite(blinker, LOW);
|
||||
|
||||
// Initialize SPIFFS
|
||||
SPIFFS.begin(true);
|
||||
if(!SPIFFS.begin(true)){
|
||||
Serial.println("An Error has occurred while mounting SPIFFS");
|
||||
return;
|
||||
}
|
||||
if(!SPIFFS.begin()){
|
||||
Serial.println("An Error has occurred while mounting SPIFFS");
|
||||
return;
|
||||
}
|
||||
|
||||
// Read in existing data
|
||||
yourInputCustomMsg = readFile(SPIFFS, "/inputCustomMsg.txt");
|
||||
yourInputSend = readFile(SPIFFS, "/inputSend.txt").toInt();
|
||||
yourInputWPM = readFile(SPIFFS, "/inputWPM.txt").toFloat();
|
||||
yourInputMsg = readFile(SPIFFS, "/inputMsg.txt").toInt();
|
||||
yourInputFloat = readFile(SPIFFS, "/inputFloat.txt").toFloat();
|
||||
yourInputStartTimeUnix = readFile(SPIFFS, "/inputStartTimeUnix.txt").toInt();
|
||||
yourInputStepLength = readFile(SPIFFS, "/inputStepLength.txt").toInt();
|
||||
yourInputCycleID = readFile(SPIFFS, "/inputCycleID.txt").toInt();
|
||||
yourInputNtransmitters = readFile(SPIFFS, "/inputNtransmitters.txt").toInt();
|
||||
// Set WPM from saved value
|
||||
sender_blink.setWPM(yourInputWPM);
|
||||
sender_key.setWPM(yourInputWPM);
|
||||
|
||||
// On restart, keep doing what you were doing before
|
||||
yourInputMsg_old = yourInputMsg;
|
||||
if(yourInputMsg == 0){
|
||||
sender_blink.setMessage(yourInputCustomMsg);
|
||||
sender_key.setMessage(yourInputCustomMsg);
|
||||
} else if(yourInputMsg == 1){
|
||||
sender_blink.setMessage(String("moe "));
|
||||
sender_key.setMessage(String("moe "));
|
||||
} else if(yourInputMsg == 2){
|
||||
sender_blink.setMessage(String("moi "));
|
||||
sender_key.setMessage(String("moi "));
|
||||
} else if(yourInputMsg == 3){
|
||||
sender_blink.setMessage(String("mos "));
|
||||
sender_key.setMessage(String("mos "));
|
||||
} else if(yourInputMsg == 4){
|
||||
sender_blink.setMessage(String("moh "));
|
||||
sender_key.setMessage(String("moh "));
|
||||
} else if(yourInputMsg == 5){
|
||||
sender_blink.setMessage(String("mo5 "));
|
||||
sender_key.setMessage(String("mo5 "));
|
||||
}
|
||||
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(ssid, password);
|
||||
if (WiFi.waitForConnectResult() != WL_CONNECTED) {
|
||||
Serial.println("WiFi Failed!");
|
||||
return;
|
||||
}
|
||||
Serial.println();
|
||||
Serial.print("IP Address: ");
|
||||
Serial.println(WiFi.localIP());
|
||||
|
||||
// Send web page with input fields to client
|
||||
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||
request->send_P(200, "text/html", index_html, processor);
|
||||
});
|
||||
|
||||
// Send a GET request to <ESP_IP>/get?inputCustomMsg=<inputMessage>
|
||||
server.on("/get", HTTP_GET, [] (AsyncWebServerRequest *request) {
|
||||
String inputMessage;
|
||||
// GET inputCustomMsg value on <ESP_IP>/get?inputCustomMsg=<inputMessage>
|
||||
if (request->hasParam(PARAM_CMSG)) {
|
||||
inputMessage = request->getParam(PARAM_CMSG)->value();
|
||||
// arduinomorse needs lowercase characters
|
||||
std::transform(inputMessage.begin(), inputMessage.end(), inputMessage.begin(), ::tolower);
|
||||
writeFile(SPIFFS, "/inputCustomMsg.txt", inputMessage.c_str());
|
||||
yourInputCustomMsg = inputMessage;
|
||||
|
||||
}
|
||||
// GET inputSend value on <ESP_IP>/get?inputSend=<inputMessage>
|
||||
if (request->hasParam(PARAM_SEND)) {
|
||||
inputMessage = request->getParam(PARAM_SEND)->value();
|
||||
writeFile(SPIFFS, "/inputSend.txt", inputMessage.c_str());
|
||||
yourInputSend = inputMessage.toInt();
|
||||
// if not running a program, set the program running off
|
||||
//if(yourInputSend != 2){
|
||||
// Cease all programs on new input
|
||||
startProgram = false;
|
||||
programRunning = false;
|
||||
//}
|
||||
}
|
||||
// GET inputWPM value on <ESP_IP>/get?inputWPM=<inputMessage>
|
||||
if (request->hasParam(PARAM_WPM)) {
|
||||
inputMessage = request->getParam(PARAM_WPM)->value();
|
||||
writeFile(SPIFFS, "/inputWPM.txt", inputMessage.c_str());
|
||||
yourInputWPM = inputMessage.toFloat();
|
||||
sender_blink.setWPM(yourInputWPM);
|
||||
sender_key.setWPM(yourInputWPM);
|
||||
}
|
||||
// GET inputMsg value on <ESP_IP>/get?inputMsg=<inputMessage>
|
||||
if (request->hasParam(PARAM_MSG)) {
|
||||
inputMessage = request->getParam(PARAM_MSG)->value();
|
||||
writeFile(SPIFFS, "/inputMsg.txt", inputMessage.c_str());
|
||||
// save previous state
|
||||
yourInputMsg_old = yourInputMsg;
|
||||
yourInputMsg = inputMessage.toInt();
|
||||
|
||||
// Check the message every time the form is submitted.
|
||||
if(yourInputMsg == 0){
|
||||
sender_blink.setMessage(yourInputCustomMsg);
|
||||
sender_key.setMessage(yourInputCustomMsg);
|
||||
} else if(yourInputMsg == 1){
|
||||
sender_blink.setMessage(String("moe "));
|
||||
sender_key.setMessage(String("moe "));
|
||||
} else if(yourInputMsg == 2){
|
||||
sender_blink.setMessage(String("moi "));
|
||||
sender_key.setMessage(String("moi "));
|
||||
} else if(yourInputMsg == 3){
|
||||
sender_blink.setMessage(String("mos "));
|
||||
sender_key.setMessage(String("mos "));
|
||||
} else if(yourInputMsg == 4){
|
||||
sender_blink.setMessage(String("moh "));
|
||||
sender_key.setMessage(String("moh "));
|
||||
} else if(yourInputMsg == 5){
|
||||
sender_blink.setMessage(String("mo5 "));
|
||||
sender_key.setMessage(String("mo5 "));
|
||||
}
|
||||
}
|
||||
// GET inputStepLength value on <ESP_IP>/get?inputStepLength=<inputMessage>
|
||||
if (request->hasParam(PARAM_STEPLENGTH)) {
|
||||
inputMessage = request->getParam(PARAM_STEPLENGTH)->value();
|
||||
writeFile(SPIFFS, "/inputStepLength.txt", inputMessage.c_str());
|
||||
yourInputStepLength = inputMessage.toInt();
|
||||
}
|
||||
// GET inputCycleID value on <ESP_IP>/get?inputCycleID=<inputMessage>
|
||||
if (request->hasParam(PARAM_CYCLEID)) {
|
||||
inputMessage = request->getParam(PARAM_CYCLEID)->value();
|
||||
writeFile(SPIFFS, "/inputCycleID.txt", inputMessage.c_str());
|
||||
yourInputCycleID = inputMessage.toInt();
|
||||
}
|
||||
// GET inputNtransmitters value on <ESP_IP>/get?inputNtransmitters=<inputMessage>
|
||||
if (request->hasParam(PARAM_NTRANS)) {
|
||||
inputMessage = request->getParam(PARAM_NTRANS)->value();
|
||||
writeFile(SPIFFS, "/inputNtransmitters.txt", inputMessage.c_str());
|
||||
yourInputNtransmitters = inputMessage.toInt();
|
||||
}
|
||||
// GET inputTimeUnix value on <ESP_IP>/get?inputTimeUnix=<inputMessage>
|
||||
if (request->hasParam(PARAM_TIME)) {
|
||||
inputMessage = request->getParam(PARAM_TIME)->value();
|
||||
//https://stackoverflow.com/a/22733127/2152245
|
||||
yourInputTime = atol(inputMessage.c_str());
|
||||
Serial.print("yourInputTime: ");
|
||||
Serial.println(yourInputTime);
|
||||
// update the RTC time
|
||||
rtc.adjust(DateTime(yourInputTime));
|
||||
DateTime now = rtc.now();
|
||||
// Might work to fix random errors? If date is far in the future,
|
||||
// try to update again.
|
||||
// replace if with while if you want it to try a bunch...
|
||||
if(now.year() > 2040){
|
||||
Serial.print("Year is ");
|
||||
Serial.println(now.year());
|
||||
Serial.println("RTC can't set time. Trying again.");
|
||||
rtc.adjust(DateTime(yourInputTime));
|
||||
}
|
||||
|
||||
Serial.print("UTC time from browser: ");
|
||||
Serial.print(now.year(), DEC);
|
||||
Serial.print('/');
|
||||
Serial.print(now.month(), DEC);
|
||||
Serial.print('/');
|
||||
Serial.print(now.day(), DEC);
|
||||
Serial.print(" (");
|
||||
Serial.print(now.dayOfTheWeek());
|
||||
Serial.print(") ");
|
||||
Serial.print(now.hour(), DEC);
|
||||
Serial.print(':');
|
||||
Serial.print(now.minute(), DEC);
|
||||
Serial.print(':');
|
||||
Serial.print(now.second(), DEC);
|
||||
Serial.println();
|
||||
|
||||
Serial.print("rtc.now().unixtime(): ");
|
||||
Serial.println(rtc.now().unixtime());
|
||||
}
|
||||
// GET inputFloat value on <ESP_IP>/get?inputFloat=<inputMessage>
|
||||
if (request->hasParam(PARAM_FLOAT)) {
|
||||
inputMessage = request->getParam(PARAM_FLOAT)->value();
|
||||
writeFile(SPIFFS, "/inputFloat.txt", inputMessage.c_str());
|
||||
yourInputFloat = inputMessage.toFloat();
|
||||
}
|
||||
// GET inputStartTimeUnix value on <ESP_IP>/get?inputStartTimeUnix=<inputMessage>
|
||||
if (request->hasParam(PARAM_START)) {
|
||||
inputMessage = request->getParam(PARAM_START)->value();
|
||||
Serial.println(inputMessage);
|
||||
// if a start time isn't entered, don't overwrite the old one
|
||||
//if(!(inputMessage != NULL && inputMessage[0] == '\0')){
|
||||
writeFile(SPIFFS, "/inputStartTimeUnix.txt", inputMessage.c_str());
|
||||
yourInputStartTimeUnix = atol(inputMessage.c_str());
|
||||
//}
|
||||
Serial.println(yourInputStartTimeUnix);
|
||||
|
||||
// Use alarm built into RTC
|
||||
rtc.setAlarm1(DateTime(yourInputStartTimeUnix), DS3231_A1_Date);
|
||||
//rtc.setAlarm1(DateTime(2020, 6, 25, 15, 34, 0), DS3231_A2_Date);
|
||||
DateTime alarm_one = rtc.getAlarm1(); // Get the current alarm time
|
||||
char buff[] = "Alarm 1 set for at hh:mm:ss DDD, DD MMM YYYY";
|
||||
Serial.print(alarm_one.toString(buff));
|
||||
Serial.println(" (only HH:MM:SS day-of-month are accurate)");
|
||||
}
|
||||
// https://techtutorialsx.com/2018/01/14/esp32-arduino-http-server-external-and-internal-redirects/
|
||||
request->redirect("/");
|
||||
});
|
||||
server.onNotFound(notFound);
|
||||
server.begin();
|
||||
|
||||
}
|
||||
|
||||
void loop() {
|
||||
|
||||
// This statement from https://github.com/garrysblog/DS3231-Alarm-With-Adafruit-RTClib-Library/blob/master/DS3231-RTClib-Adafruit-Alarm-Poll-alarmFired/DS3231-RTClib-Adafruit-Alarm-Poll-alarmFired.ino
|
||||
// Check if alarm by polling SQW alarm pin
|
||||
if((yourInputSend == 2) & (digitalRead(alarmPin) == LOW)) {
|
||||
// Print current time and date
|
||||
DateTime now = rtc.now(); // Get the current time
|
||||
char buff[] = "Alarm triggered at hh:mm:ss DDD, DD MMM YYYY";
|
||||
Serial.println(now.toString(buff));
|
||||
startProgram = true;
|
||||
|
||||
// Disable and clear alarm
|
||||
rtc.clearAlarm(1);
|
||||
rtc.clearAlarm(2); // clear the other one just in case
|
||||
}
|
||||
|
||||
// Once alarm has started the program, set things up to run
|
||||
if(startProgram == true){
|
||||
//Serial.println("Start sending");
|
||||
start_millis = millis() + ((yourInputCycleID - 1) * yourInputStepLength);
|
||||
stop_millis = start_millis + yourInputStepLength;
|
||||
if(yourInputCycleID == 1){
|
||||
pause_until_millis = stop_millis + (yourInputStepLength * (yourInputNtransmitters - 1));
|
||||
} else {
|
||||
// Subtract 2 rather than 1 here to account for start_millis duration at beginning of repeat.
|
||||
pause_until_millis = stop_millis + (yourInputStepLength * (yourInputNtransmitters - 2));
|
||||
}
|
||||
programRunning = true;
|
||||
startProgram = false;
|
||||
}
|
||||
|
||||
// if you want to send continuous code, and it's not sending, then start it up
|
||||
if((yourInputSend == 1)){
|
||||
// If not sending, start sending. Yes, these need to be separate statements.
|
||||
if (!sender_blink.continueSending()){
|
||||
sender_blink.startSending();
|
||||
}
|
||||
if (!sender_key.continueSending()){
|
||||
sender_key.startSending();
|
||||
}
|
||||
// if you want to send cycle code and it's not sending, then start it up
|
||||
} else if((yourInputSend == 2) & (programRunning == true)){
|
||||
if((millis() < start_millis)){
|
||||
// Shut the pin off manually
|
||||
digitalWrite(blinker, LOW);
|
||||
digitalWrite(keyer, LOW);
|
||||
} else if((millis() >= start_millis) & (millis() <= stop_millis)){
|
||||
// If not sending, start sending. Yes, these need to be separate statements
|
||||
// for the blinker and keyer.
|
||||
if (!sender_blink.continueSending()){
|
||||
sender_blink.startSending();
|
||||
}
|
||||
if (!sender_key.continueSending()){
|
||||
sender_key.startSending();
|
||||
}
|
||||
} else if((millis() >= stop_millis) & (millis() <= pause_until_millis)){
|
||||
// do nothing in this case -- in between cycles
|
||||
// Shut the pin off manually
|
||||
digitalWrite(blinker, LOW);
|
||||
digitalWrite(keyer, LOW);
|
||||
} else if((millis() >= pause_until_millis)){
|
||||
startProgram = true;
|
||||
}
|
||||
// if the cycle program is not running
|
||||
} else if((yourInputSend == 2) & (programRunning == false)){
|
||||
// do we need something here?
|
||||
// if you don't want to send code
|
||||
} else if(yourInputSend == 0){
|
||||
// Shut the pin off manually
|
||||
digitalWrite(blinker, LOW);
|
||||
digitalWrite(keyer, LOW);
|
||||
}
|
||||
|
||||
}
|
||||
Executable
+232
@@ -0,0 +1,232 @@
|
||||
// Morse Code sending library
|
||||
|
||||
#include <morse.h>
|
||||
|
||||
// MorseSender
|
||||
int MorseSender::copyTimings(
|
||||
morseTiming_t *rawOut,
|
||||
morseBitmask_t definition)
|
||||
{
|
||||
int t = 0;
|
||||
boolean foundSentinel = false;
|
||||
for(morseBitmask_t mask = MORSE_BITMASK_HIGH_BIT;
|
||||
mask > 0; mask = mask >> 1)
|
||||
{
|
||||
boolean isDah = (mask & definition) > 0;
|
||||
if(!foundSentinel)
|
||||
{
|
||||
if (isDah) { foundSentinel = true; }
|
||||
continue;
|
||||
}
|
||||
rawOut[2*t] = isDah ? DAH : DIT;
|
||||
rawOut[2*t + 1] = DIT;
|
||||
t++;
|
||||
}
|
||||
return t;
|
||||
}
|
||||
unsigned int MorseSender::fillTimings(char c)
|
||||
{
|
||||
int t = 0;
|
||||
unsigned int start = 0;
|
||||
if (c >= 'a' && c <= 'z')
|
||||
{
|
||||
t = copyTimings(timingBuffer, MORSE_LETTERS[c-'a']);
|
||||
}
|
||||
else if (c >= '0' && c <= '9')
|
||||
{
|
||||
int n = c - '0';
|
||||
boolean ditsFirst = (n <= 5);
|
||||
if (!ditsFirst)
|
||||
{
|
||||
n -= 5;
|
||||
}
|
||||
while(t < 5)
|
||||
{
|
||||
timingBuffer[2*t] = ((t < n) == ditsFirst) ? DIT : DAH;
|
||||
timingBuffer[2*t + 1] = DIT;
|
||||
t++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int s = 0;
|
||||
while(MORSE_PUNCT_ETC[s].c != END)
|
||||
{
|
||||
if(MORSE_PUNCT_ETC[s].c == c)
|
||||
{
|
||||
t = copyTimings(timingBuffer,
|
||||
MORSE_PUNCT_ETC[s].timing);
|
||||
break;
|
||||
}
|
||||
s++;
|
||||
}
|
||||
if (MORSE_PUNCT_ETC[s].c == END)
|
||||
{
|
||||
start = t = 1; // start on a space
|
||||
}
|
||||
}
|
||||
|
||||
timingBuffer[2*t - 1] = DAH;
|
||||
timingBuffer[2*t] = END;
|
||||
|
||||
/*
|
||||
Serial.print("Refilled timing buffer for '");
|
||||
Serial.print(c);
|
||||
Serial.print("': ");
|
||||
int i = start;
|
||||
while(timingBuffer[i] != END)
|
||||
{
|
||||
Serial.print((int)timingBuffer[i]);
|
||||
Serial.print(", ");
|
||||
i++;
|
||||
}
|
||||
Serial.println("END");
|
||||
*/
|
||||
|
||||
return start;
|
||||
}
|
||||
|
||||
// see note in header about pure-virtual-ness
|
||||
void MorseSender::setOn() {};
|
||||
void MorseSender::setOff() {};
|
||||
|
||||
// noop defaults
|
||||
void MorseSender::setReady() {};
|
||||
void MorseSender::setComplete() {};
|
||||
|
||||
MorseSender::MorseSender(unsigned int outputPin, float wpm) :
|
||||
pin(outputPin)
|
||||
{
|
||||
setWPM(wpm);
|
||||
}
|
||||
|
||||
void MorseSender::setup() { pinMode(pin, OUTPUT); }
|
||||
|
||||
void MorseSender::setWPM(float wpm)
|
||||
{
|
||||
setSpeed((morseTiming_t)(1000.0*60.0/(max(1.0f, wpm)*DITS_PER_WORD)));
|
||||
}
|
||||
|
||||
void MorseSender::setSpeed(morseTiming_t duration)
|
||||
{
|
||||
DIT = max(duration, (morseTiming_t) 1);
|
||||
DAH = 3*DIT;
|
||||
}
|
||||
|
||||
void MorseSender::setMessage(const String newMessage)
|
||||
{
|
||||
message = newMessage;
|
||||
|
||||
// Force startSending() before continueSending().
|
||||
messageIndex = message.length();
|
||||
|
||||
// If a different message was in progress, make sure it stops cleanly.
|
||||
if (timingIndex % 2 == 0) {
|
||||
setOff();
|
||||
}
|
||||
}
|
||||
|
||||
void MorseSender::sendBlocking()
|
||||
{
|
||||
//Serial.println("Sending blocking: ");
|
||||
//Serial.println(message);
|
||||
startSending();
|
||||
while(continueSending());
|
||||
}
|
||||
|
||||
void MorseSender::startSending()
|
||||
{
|
||||
messageIndex = 0;
|
||||
if (message.length() == 0) { return; }
|
||||
timingIndex = fillTimings(message[0]);
|
||||
setReady();
|
||||
if (timingIndex % 2 == 0) {
|
||||
setOn();
|
||||
//Serial.print("Starting with on, duration=");
|
||||
} else {
|
||||
//Serial.print("Starting with off, duration=");
|
||||
}
|
||||
lastChangedMillis = millis();
|
||||
//Serial.println((int)timingBuffer[timingIndex]);
|
||||
}
|
||||
|
||||
boolean MorseSender::continueSending()
|
||||
{
|
||||
if(messageIndex >= message.length()) { return false; }
|
||||
|
||||
unsigned long elapsedMillis = millis() - lastChangedMillis;
|
||||
if (elapsedMillis < timingBuffer[timingIndex]) { return true; }
|
||||
|
||||
timingIndex++;
|
||||
if (timingBuffer[timingIndex] == END)
|
||||
{
|
||||
messageIndex++;
|
||||
if(messageIndex >= message.length()) {
|
||||
setOff();
|
||||
setComplete();
|
||||
return false;
|
||||
}
|
||||
timingIndex = fillTimings(message[messageIndex]);
|
||||
}
|
||||
|
||||
lastChangedMillis += elapsedMillis;
|
||||
//Serial.print("Next is ");
|
||||
if (timingIndex % 2 == 0) {
|
||||
//Serial.print("(on) ");
|
||||
setOn();
|
||||
} else {
|
||||
//Serial.print("(off) ");
|
||||
setOff();
|
||||
}
|
||||
//Serial.println((int)timingBuffer[timingIndex]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void *MorseSender::operator new(size_t size) { return malloc(size); }
|
||||
void MorseSender::operator delete(void* ptr) { if (ptr) free(ptr); }
|
||||
|
||||
|
||||
// SpeakerMorseSender
|
||||
|
||||
// void SpeakerMorseSender::setOn() { tone(pin, frequency); }
|
||||
// void SpeakerMorseSender::setOff() {
|
||||
// if (carrFrequency == CARRIER_FREQUENCY_NONE) {
|
||||
// noTone(pin);
|
||||
// } else {
|
||||
// tone(pin, carrFrequency);
|
||||
// }
|
||||
// }
|
||||
// void SpeakerMorseSender::setReady() { setOff(); }
|
||||
// void SpeakerMorseSender::setComplete() { noTone(pin); }
|
||||
// SpeakerMorseSender::SpeakerMorseSender(
|
||||
// int outputPin,
|
||||
// unsigned int toneFrequency,
|
||||
// unsigned int carrierFrequency,
|
||||
// float wpm)
|
||||
// : MorseSender(outputPin, wpm),
|
||||
// frequency(toneFrequency),
|
||||
// carrFrequency(carrierFrequency) {};
|
||||
|
||||
|
||||
// LEDMorseSender
|
||||
|
||||
void LEDMorseSender::setOn() { digitalWrite(pin, activeLow ? LOW : HIGH); }
|
||||
void LEDMorseSender::setOff() { digitalWrite(pin, activeLow ? HIGH : LOW); }
|
||||
LEDMorseSender::LEDMorseSender(int outputPin, bool activeLow, float wpm)
|
||||
: MorseSender(outputPin, wpm), activeLow(activeLow) {};
|
||||
LEDMorseSender::LEDMorseSender(int outputPin, float wpm)
|
||||
: MorseSender(outputPin, wpm), activeLow(false) {};
|
||||
|
||||
// PWMMorseSender
|
||||
|
||||
void PWMMorseSender::setOn() { analogWrite(pin, brightness); }
|
||||
void PWMMorseSender::setOff() { analogWrite(pin, 0); }
|
||||
void PWMMorseSender::setBrightness(byte bright) {
|
||||
brightness = bright;
|
||||
}
|
||||
PWMMorseSender::PWMMorseSender(
|
||||
int outputPin,
|
||||
float wpm,
|
||||
byte bright)
|
||||
: MorseSender(outputPin, wpm), brightness(bright) {};
|
||||
Executable
+276
@@ -0,0 +1,276 @@
|
||||
#pragma once
|
||||
/**
|
||||
* Generate and send Morse Code on an LED or a speaker. Allow sending
|
||||
* in a non-blocking manner (by calling a 'continue sending' method
|
||||
* every so often to turn an LED on/off, or to call tone/noTone appropriately).
|
||||
*
|
||||
* All input should be lowercase. Prosigns (SK, KN, etc) have special
|
||||
* character values #defined.
|
||||
*
|
||||
* See also:
|
||||
* Morse decoder (using binary tree):
|
||||
* http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1289074596/15
|
||||
* Generator (on playground):
|
||||
* http://www.arduino.cc/playground/Code/Morse
|
||||
*/
|
||||
|
||||
// for malloc and free, for the new/delete operators
|
||||
#include <stdlib.h>
|
||||
#include <analogWrite.h>
|
||||
|
||||
// Arduino language types
|
||||
#if defined(ARDUINO) && ARDUINO >= 100
|
||||
#include "Arduino.h"
|
||||
#else
|
||||
#include "WProgram.h"
|
||||
#endif
|
||||
|
||||
#define WPM_DEFAULT 12.0
|
||||
// PARIS WPM measurement: 50; CODEX WPM measurement: 60 (Wikipedia:Morse_code)
|
||||
#define DITS_PER_WORD 50
|
||||
// Pass to SpeakerMorseSender as carrierFrequency to suppress the carrier.
|
||||
#define CARRIER_FREQUENCY_NONE 0
|
||||
|
||||
// Bitmasks are 1 for dah and 0 for dit, in left-to-right order;
|
||||
// the sequence proper begins after the first 1 (a sentinel).
|
||||
// Credit for this scheme to Mark VandeWettering K6HX ( brainwagon.org ).
|
||||
typedef unsigned int morseTiming_t;
|
||||
typedef unsigned char morseBitmask_t; // see also MAX_TIMINGS
|
||||
#define MORSE_BITMASK_HIGH_BIT B10000000
|
||||
|
||||
// sentinel
|
||||
#define END 0
|
||||
|
||||
// the most timing numbers any unit will need; ex: k = on,off,on,off,on,end = 5
|
||||
#define MAX_TIMINGS 15
|
||||
|
||||
// Punctuation and Prosigns
|
||||
#define PROSIGN_SK 'S'
|
||||
#define PROSIGN_KN 'K'
|
||||
#define PROSIGN_BT 'B'
|
||||
typedef struct {
|
||||
char c;
|
||||
morseBitmask_t timing;
|
||||
} specialTiming;
|
||||
const specialTiming MORSE_PUNCT_ETC[] = {
|
||||
{'.', B1010101},
|
||||
{'?', B1001100},
|
||||
{'/', B110010},
|
||||
{PROSIGN_SK, B1000101},
|
||||
{PROSIGN_KN, B110110},
|
||||
{PROSIGN_BT, B110001},
|
||||
{END, B1},
|
||||
};
|
||||
|
||||
// Morse Code (explicit declaration of letter timings)
|
||||
const morseBitmask_t MORSE_LETTERS[26] = {
|
||||
/* a */ B101,
|
||||
/* b */ B11000,
|
||||
/* c */ B11010,
|
||||
/* d */ B1100,
|
||||
/* e */ B10,
|
||||
/* f */ B10010,
|
||||
/* g */ B1110,
|
||||
/* h */ B10000,
|
||||
/* i */ B100,
|
||||
/* j */ B10111,
|
||||
/* k */ B1101,
|
||||
/* l */ B10100,
|
||||
/* m */ B111,
|
||||
/* n */ B110,
|
||||
/* o */ B1111,
|
||||
/* p */ B10110,
|
||||
/* q */ B11101,
|
||||
/* r */ B1010,
|
||||
/* s */ B1000,
|
||||
/* t */ B11,
|
||||
/* u */ B1001,
|
||||
/* v */ B10001,
|
||||
/* w */ B1011,
|
||||
/* x */ B11001,
|
||||
/* y */ B11011,
|
||||
/* z */ B11100,
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Define the logic of converting characters to on/off timing,
|
||||
* and encapsulate the state of one sending-in-progress Morse message.
|
||||
*
|
||||
* Subclasses define setOn and setOff for (for example) LED and speaker output.
|
||||
*/
|
||||
class MorseSender {
|
||||
protected:
|
||||
const unsigned int pin;
|
||||
// The setOn and setOff methods would be pure virtual,
|
||||
// but that has compiler issues.
|
||||
// See: http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1167672075 .
|
||||
|
||||
/**
|
||||
* Called to set put the output in 'on' state, during a dit or dah.
|
||||
*/
|
||||
virtual void setOn();
|
||||
virtual void setOff();
|
||||
|
||||
/**
|
||||
* Called before sending a message. Used for example to enable a
|
||||
* carrier. (Noop in the base class.)
|
||||
*/
|
||||
virtual void setReady();
|
||||
virtual void setComplete();
|
||||
|
||||
private:
|
||||
morseTiming_t DIT, DAH;
|
||||
String message;
|
||||
|
||||
// on,off,...,wait,0 list, millis
|
||||
morseTiming_t timingBuffer[MAX_TIMINGS+1];
|
||||
|
||||
// index of the character currently being sent
|
||||
unsigned int messageIndex;
|
||||
// timing unit currently being sent
|
||||
unsigned int timingIndex;
|
||||
|
||||
// when this timing unit was started
|
||||
unsigned long lastChangedMillis;
|
||||
|
||||
/**
|
||||
* Copy definition timings (on only) to raw timings (on/off).
|
||||
* @return the number of 'on' timings copied
|
||||
*/
|
||||
int copyTimings(morseTiming_t *rawOut,
|
||||
morseBitmask_t definition);
|
||||
|
||||
/**
|
||||
* Fill a buffer with on,off,..,END timings (millis)
|
||||
* @return the index at which to start within the new timing sequence
|
||||
*/
|
||||
unsigned int fillTimings(char c);
|
||||
|
||||
public:
|
||||
/**
|
||||
* Create a sender which will output to the given pin.
|
||||
*/
|
||||
MorseSender(unsigned int outputPin, float wpm=WPM_DEFAULT);
|
||||
|
||||
/**
|
||||
* To be called during the Arduino setup(); set the pin as OUTPUT.
|
||||
*/
|
||||
void setup();
|
||||
|
||||
/**
|
||||
* Set the words per minute (based on PARIS timing).
|
||||
*/
|
||||
void setWPM(float wpm);
|
||||
|
||||
/**
|
||||
* Set the duration, in milliseconds, of a DIT.
|
||||
*/
|
||||
void setSpeed(morseTiming_t duration);
|
||||
|
||||
/**
|
||||
* Set the message to be sent.
|
||||
* This halts any sending in progress.
|
||||
*/
|
||||
void setMessage(const String newMessage);
|
||||
|
||||
/**
|
||||
* Send the entirety of the current message before returning. See the "simple"
|
||||
* example, which uses sendBlocking to send one message.
|
||||
*/
|
||||
void sendBlocking();
|
||||
|
||||
/**
|
||||
* Prepare to send and begin sending the current message. After calling this,
|
||||
* call continueSending repeatedly until it returns false to finish sending
|
||||
* the message. See the "speeds" example, which calls startSending and
|
||||
* continueSending on two different senders.
|
||||
*/
|
||||
void startSending();
|
||||
|
||||
/**
|
||||
* Switch outputs on and off (and refill the internal timing buffer)
|
||||
* as necessary to continue with the sending of the current message.
|
||||
* This should be called every few milliseconds (at a significantly
|
||||
* smaller interval than a DIT) to produce a legible fist.
|
||||
*
|
||||
* @see startSending, which must be called first
|
||||
* @return false if sending is complete, otherwise true (keep sending)
|
||||
*/
|
||||
boolean continueSending();
|
||||
|
||||
void *operator new(size_t size);
|
||||
void operator delete(void* ptr);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Adapt Morse sending to use the Arduino language tone() and noTone()
|
||||
* functions, for use with a speaker.
|
||||
*
|
||||
* If a carrierFrequency is given, instead of calling noTone, call tone
|
||||
* with a low frequency. This is useful ex. for maintaining radio links.
|
||||
*/
|
||||
class SpeakerMorseSender: public MorseSender {
|
||||
private:
|
||||
unsigned int frequency;
|
||||
unsigned int carrFrequency;
|
||||
protected:
|
||||
virtual void setOn();
|
||||
virtual void setOff();
|
||||
virtual void setReady();
|
||||
virtual void setComplete();
|
||||
public:
|
||||
// concert A = 440
|
||||
// middle C = 261.626; higher octaves = 523.251, 1046.502
|
||||
SpeakerMorseSender(
|
||||
int outputPin,
|
||||
unsigned int toneFrequency=1046,
|
||||
unsigned int carrierFrequency=CARRIER_FREQUENCY_NONE,
|
||||
float wpm=WPM_DEFAULT);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sends Morse on a digital output pin.
|
||||
*/
|
||||
class LEDMorseSender: public MorseSender {
|
||||
private:
|
||||
bool activeLow;
|
||||
protected:
|
||||
virtual void setOn();
|
||||
virtual void setOff();
|
||||
public:
|
||||
/**
|
||||
* Creates a LED Morse code sender with the given GPIO pin. The optional
|
||||
* boolean activeLow indicates LED is ON with digital LOW value.
|
||||
* @param outputPin GPIO pin number
|
||||
* @param activeLow set to true to indicate the LED ON with digital LOW value. default: false
|
||||
* @param wpm words per minute, default: WPM_DEFAULT
|
||||
*/
|
||||
LEDMorseSender(int outputPin, bool activeLow = false, float wpm=WPM_DEFAULT);
|
||||
|
||||
/**
|
||||
* Creates a LED Morse code sender with the given GPIO pin. This constructor is for backward compability.
|
||||
* @param outputPin GPIO pin number
|
||||
* @param wpm words per minute
|
||||
*/
|
||||
LEDMorseSender(int outputPin, float wpm);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sends Morse on an analog output pin (using PWM). The brightness value is
|
||||
* between 0 and 255 and is passed directly to analogWrite.
|
||||
*/
|
||||
class PWMMorseSender: public MorseSender {
|
||||
private:
|
||||
byte brightness;
|
||||
protected:
|
||||
virtual void setOn();
|
||||
virtual void setOff();
|
||||
public:
|
||||
PWMMorseSender(int outputPin, float wpm=WPM_DEFAULT, byte brightness=255);
|
||||
void setBrightness(byte brightness);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
|
||||
This directory is intended for PlatformIO Test Runner and project tests.
|
||||
|
||||
Unit Testing is a software testing method by which individual units of
|
||||
source code, sets of one or more MCU program modules together with associated
|
||||
control data, usage procedures, and operating procedures, are tested to
|
||||
determine whether they are fit for use. Unit testing finds problems early
|
||||
in the development cycle.
|
||||
|
||||
More information about PlatformIO Unit Testing:
|
||||
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html
|
||||
Reference in New Issue
Block a user