87 Commits

Author SHA1 Message Date
mattbk 269cbfdd22 Shelve. 2023-09-16 19:17:31 -05:00
mattbk c48c624f71 Shelve. 2023-09-16 17:04:22 -05:00
W1CDN dccd3d27f4 Merge pull request 'Add custom message ability' (#47) from custom-message into main
Reviewed-on: #47
2023-09-16 15:48:03 -05:00
mattbk 2618d82e12 Add custom message ability 2023-09-16 09:51:39 -05:00
W1CDN 08b1bdf3fd Merge pull request 'Change WPM on webform' (#45) from wpm into main
Reviewed-on: #45
2023-09-16 09:17:17 -05:00
mattbk d973bbf19a Clean up. 2023-09-16 09:16:35 -05:00
mattbk f4287eba7b Control WPM from webform. 2023-09-15 21:44:19 -05:00
W1CDN 3474d8ba17 Merge pull request 'Key radio as well as blinking LED' (#44) from key-radio into main
Reviewed-on: #44
2023-09-15 21:19:50 -05:00
mattbk ffb4163980 Wire up keyer. 2023-09-15 19:58:46 -05:00
W1CDN 3db888e530 Merge pull request 'Set up scheduled program cycles' (#24) from program-cycle into main
Reviewed-on: #24
2023-09-15 16:51:44 -05:00
W1CDN a93498ac84 Merge pull request 'Use arduinomorse instead of Jled' (#38) from arduinomorse into program-cycle
Reviewed-on: #38
2023-09-15 16:35:46 -05:00
mattbk 6c1a718a98 Do the math right on cycle timing. 2023-09-15 12:22:02 -05:00
mattbk 4380a56055 Shelve. 2023-09-14 21:02:28 -05:00
mattbk f5aa43ef76 Clean out old Jled and arduino-timer code. 2023-09-14 17:09:55 -05:00
mattbk c8f9d823da Add back ability to send continuously. 2023-09-14 16:36:22 -05:00
mattbk a47cd1465a Clean up. 2023-09-14 12:46:35 -05:00
mattbk 73a6b694a9 Cycle appropriately. 2023-09-14 12:44:10 -05:00
mattbk 06e69bd8c0 Start on schedule and able to stop. 2023-09-13 21:54:43 -05:00
mattbk dd085635a2 Shelve. 2023-09-13 21:06:16 -05:00
mattbk 087ad27c8c Add arduinomorse and dependencies. 2023-09-13 20:24:57 -05:00
mattbk 5c655a381b Snapshot. 2023-09-13 19:58:45 -05:00
mattbk dc765af473 Working snapshot. 2023-09-12 21:32:11 -05:00
mattbk 3aba0583be Stub out a non-working function to set up Jled sequences. 2023-09-11 21:47:37 -05:00
mattbk 1bde39e148 Snapshot. 2023-09-11 21:09:27 -05:00
mattbk 958e71513c Snapshot. 2023-09-10 12:35:02 -05:00
mattbk 70decbbcca Only send cycles when program is running. 2023-09-09 16:01:41 -05:00
mattbk 2127d4a75a Add extra space at end of message to workaround Jled bug. 2023-09-09 13:14:28 -05:00
mattbk 40baa679e1 Working cycle example with hardcoded variables. 2023-09-09 12:53:34 -05:00
mattbk e706327623 Shelve. 2023-09-08 22:55:31 -05:00
mattbk fbe5a4a6e0 Snapshot. 2023-09-08 21:21:19 -05:00
mattbk 26a1ee97c0 Delete a bunch of old stuff. 2023-09-08 15:07:57 -05:00
mattbk cbdc7ec939 Use Alarm2 instead of Alarm2 to avoid late alarms. 2023-09-08 15:00:48 -05:00
mattbk 38c1417351 Report time alarm is set (serial). 2023-09-08 11:12:44 -05:00
mattbk 57a1c1af80 Snapshot. 2023-09-08 10:39:24 -05:00
mattbk af4920d634 Snapshot. 2023-09-08 08:39:14 -05:00
mattbk a69128397a Snapshot. 2023-09-07 20:20:53 -05:00
mattbk f496a10ef2 Try to account for infrequent weird RTC times. 2023-09-06 22:07:48 -05:00
mattbk afe5b9338d Keep form fields updated. 2023-09-06 21:52:04 -05:00
mattbk a996c13e63 Snapshot to capture working refresh... 2023-09-06 21:36:10 -05:00
mattbk dab1590608 Use unix timestamps and convert to/from local in js. 2023-09-06 21:21:05 -05:00
mattbk 90b3137165 Shelve. 2023-09-06 20:14:16 -05:00
mattbk 55ea853100 Ignore vscode settings. 2023-09-06 19:24:03 -05:00
mattbk b97f48858d Quick fix of empty start time. 2023-09-06 18:08:36 -05:00
mattbk b648900c7b Shelve. 2023-09-06 18:02:07 -05:00
mattbk c174d7f594 Make date formats match and stop passing an extra variable. 2023-09-06 16:57:57 -05:00
mattbk fd61efebba Get on local time, baby. 2023-09-05 21:11:31 -05:00
mattbk 8a95413224 Snapshot. 2023-09-05 20:41:55 -05:00
mattbk 458232f08f Get dates lined up, readable, and values refreshed. 2023-09-05 16:55:42 -05:00
mattbk a47451b541 Get time and start time aligned. 2023-09-05 15:41:56 -05:00
mattbk 2060df9691 Refresh webform automatically after submit so values are right. 2023-09-05 11:08:01 -05:00
mattbk c65bc22028 Shelve.
Collect unix time from webform, but need to make sure it is UTC.
2023-09-04 21:53:09 -05:00
W1CDN 6807d3f56f Merge pull request 'Fix lockup and message change' (#21) from debug-new-message into main
Reviewed-on: #21
2023-09-04 20:48:14 -05:00
mattbk 07db0cf748 Add example config file. 2023-09-04 20:41:07 -05:00
mattbk 4c6105734e Blink LED while sending CW. 2023-09-04 20:23:14 -05:00
mattbk a3ec425c88 Set old msg to new msg (fix lockup) and change CQ to TEST. 2023-09-04 17:26:03 -05:00
W1CDN 48db6bc4c9 Merge pull request 'Integrate RTC chip' (#16) from add-rtc into main
Reviewed-on: #16
2023-09-03 11:00:46 -05:00
mattbk d4db1fa1f8 Push current time back to web page. 2023-09-03 10:56:54 -05:00
mattbk d5852bb3bb Clean up JS. 2023-09-02 22:13:11 -05:00
mattbk abe751d30c Get form submission time from webform and update RTC. 2023-09-02 22:11:51 -05:00
mattbk 0317a93d38 Shelve. 2023-09-02 20:02:59 -05:00
W1CDN ca53478b3c Merge pull request 'Webform switch between continuous and cycle sending' (#12) from continuous-or-cycle into main
Reviewed-on: #12
2023-09-02 18:49:24 -05:00
mattbk 736109b9b0 Revert "Stub out RTC integration."
This reverts commit 7d673fe70c.
2023-09-02 18:48:40 -05:00
mattbk 5f6cfd653c Stub out RTC integration. 2023-09-02 18:45:50 -05:00
mattbk 7d673fe70c Stub out RTC integration. 2023-09-02 18:35:08 -05:00
mattbk bfc43443b4 Clarify some things. 2023-09-01 14:15:54 -05:00
mattbk 20e075d29d Send previous message on startup. 2023-09-01 10:01:49 -05:00
mattbk 6af5dfcd0e Choose message to send.
This only works after you submit the webform again. More work needed so it works on start.
2023-09-01 09:39:00 -05:00
mattbk cc4d798a03 Get back to working. 2023-08-31 23:09:21 -05:00
mattbk 50eaf8e973 Snapshot. 2023-08-31 21:35:49 -05:00
mattbk b0be20087c Add option for cycle send and clean up a bit. 2023-08-31 20:41:24 -05:00
mattbk 13ddf853ed Use a dropdown for program setting. 2023-08-31 11:52:11 -05:00
mattbk 17d8883f39 Merge pull request 'Group inputs into one webform' (#5) from share-webform into main
Reviewed-on: #5
2023-08-31 11:20:38 -05:00
mattbk 6552d9034f Refer to global variables rather than reading files. 2023-08-31 09:10:30 -05:00
mattbk 9b928cb11b Combine forms and remove elses in form processing. 2023-08-30 22:26:23 -05:00
mattbk b673520a39 Organize files. 2023-08-30 22:10:29 -05:00
mattbk 3977971341 Get basic Morse LED control working with JLed. 2023-08-30 22:04:32 -05:00
mattbk 7f4c8c001e Find a Morse library that actually works. 2023-08-29 20:24:17 -05:00
mattbk e573c1ed1b Snapshot. 2023-08-27 22:35:16 -05:00
mattbk 917dd85465 Add function to toggle gpio 26. 2023-08-26 21:08:35 -05:00
mattbk 5b9e369e7a VSCode needs a workspace, I guess. 2023-08-26 20:55:52 -05:00
mattbk 856ada13e8 Add simple timer example. 2023-08-26 20:55:28 -05:00
mattbk 76ef0b129f Blink LED according to time entered. 2023-08-22 14:13:47 -05:00
mattbk df8e3af432 Add default files from VSCode. 2023-08-22 12:28:08 -05:00
mattbk d2e0b6ef97 Remove extra gitignore. 2023-08-22 12:27:18 -05:00
mattbk 969680fba3 Working webform and save to flash. 2023-08-22 12:23:47 -05:00
mattbk ab7ea696ca PlatformIO 2023-08-22 12:21:21 -05:00
Matt d096346247 Reinitialize. 2023-08-22 03:06:02 +00:00
27 changed files with 1797 additions and 655 deletions
-4
View File
@@ -1,4 +0,0 @@
/logs/*
config.ini
*.db
*.log
-80
View File
@@ -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
View File
@@ -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
-12
View File
@@ -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)
-20
View File
@@ -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
-49
View File
@@ -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("");
}
}
+157
View File
@@ -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
View File
@@ -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
View File
@@ -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()
-8
View File
@@ -1,8 +0,0 @@
flask
flask_restful
aprs3
kiss3
kiss
aprslib
sqlite3
json
-61
View File
@@ -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)
);
-4
View File
@@ -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
-59
View File
@@ -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
View File
@@ -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()
+8
View File
@@ -0,0 +1,8 @@
{
"folders": [
{
"path": "."
}
],
"settings": {}
}
+7
View File
@@ -0,0 +1,7 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch
.vscode/*
*/config.h
+10
View File
@@ -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"
]
}
+39
View File
@@ -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
+46
View File
@@ -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
+26
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
#define WIFI_SSID "wifi_name"
#define WIFI_PASSWORD "wifi_pass"
+631
View File
@@ -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);
}
}
+232
View File
@@ -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) {};
+276
View File
@@ -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);
};
+11
View File
@@ -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