Last active
June 23, 2024 12:19
-
-
Save bd1es/262569cd516ed16019e4ced12fa82327 to your computer and use it in GitHub Desktop.
time2tty, for a NanoPi NEO2 running Armbian
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* time2tty.cpp | |
* | |
* This file is part of the QTR Clock AVR, my project for displaying time using 8051 or AVR. | |
* | |
* time2tty, version 1.0 Copyright (C) 2010-2023 BD1ES. | |
* | |
* This program is free software: you can redistribute it and/or modify | |
* it under the terms of the GNU General Public License as published by | |
* the Free Software Foundation, either version 3 of the License, or | |
* (at your option) any later version. | |
* | |
* This program is distributed in the hope that it will be useful, | |
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
* GNU General Public License for more details. | |
* | |
* You should have received a copy of the GNU General Public License | |
* along with this program. If not, see <https://www.gnu.org/licenses/>. | |
*/ | |
/* | |
* This program, time2tty, is designed for debugging the serial communication of QTR Clocks. | |
* | |
* The feature includes: | |
* Test the text message (containing the time and timing information): | |
* Send the generated message to the serial port; | |
* Print the received message to the console; | |
* Test the QTR message (the Hamming-encoded "type 1" message): | |
* Send the generated message to the serial port; | |
* Print the decoded content and the measured transmission latency to the console; | |
* Test the TOD message (the NMEA-0183 ZDA sentence): | |
* Send the generated message to the serial port; | |
* Print the decoded content and the measured transmission latency to the console; | |
* | |
* The source code can be compiled with C++11-compliant compilers, such as GCC, Clang, and MinGW- | |
* w64, distributed/installed with Armbian, Raspberry Pi OS, Ubuntu, FreeBSD, macOS, and Windows. | |
* | |
* (Dev-C++ 5.11 with built-in TDM-GCC 4.9.2 is an option for building standalone executables for | |
* Windows 2000 and XP.) | |
* | |
* time2tty makes use of the following open-source software: | |
* libserialport, https://sigrok.org/wiki/Libserialport | |
* rang, https://github.com/agauniyal/rang | |
* fmt, https://github.com/fmtlib/fmt | |
* y2038, https://github.com/evalEmpire/y2038 | |
* eventpp, https://github.com/wqking/eventpp | |
* | |
* Author: BD1ES | |
* Last modified: Sep, 2023 | |
*/ | |
#include <rang.hpp> | |
#include "time2tty.h" | |
using namespace rang; | |
// ------------------------------------------------------------------------------------------------ | |
struct message_base_t | |
{ | |
void print_text(const std::vector<uint8_t>& msg) | |
{ | |
for(auto c : msg) if(isprint(c)) std::cout << c; | |
} | |
void print_hex(const std::vector<uint8_t>& msg) | |
{ | |
hex_print_t hex; | |
for(auto c : msg) hex.update(c); | |
hex.final(); | |
} | |
static void perr(const std::string& msg) | |
{ | |
std::cerr << style::bold << fgB::red << msg << style::reset << std::endl; | |
} | |
protected: | |
// This type prints the given bytes as hex numbers using a layout similar to Vim. | |
struct hex_print_t | |
{ | |
hex_print_t(size_t base_addr = 0) | |
{ | |
current_addr = base_addr; | |
} | |
void set_base_addr(size_t base_addr) | |
{ | |
current_addr = base_addr; | |
} | |
void update(uint8_t d) | |
{ | |
current_addr++; | |
buf.push_back(d); | |
if(buf.size() == 16) print_line(); | |
} | |
void final() | |
{ | |
if(!empty()) print_line(); | |
} | |
bool empty() | |
{ | |
return buf.empty(); | |
} | |
private: | |
std::vector<uint8_t> buf; | |
size_t current_addr; | |
void print_line() | |
{ | |
std::cout << fmt::format("{:010}: ", current_addr - buf.size()); | |
uint8_t j = 0; | |
for(auto c : buf){ // print "c" as hex numbers | |
if((j++ % 2) == 0) std::cout << ' '; | |
std::cout << fmt::format("{:02X}", c); | |
} | |
while(j < 16){ // print fillers | |
if((j++ % 2) == 0){ | |
std::cout << " "; | |
}else{ | |
std::cout << " "; | |
} | |
} | |
std::cout << " "; | |
for(auto c: buf){ // print "c" as characters | |
if(isprint(c)){ | |
std::cout << c; | |
}else{ | |
std::cout << '.'; | |
} | |
} | |
std::cout << std::endl; | |
buf.clear(); | |
} | |
}; | |
}; | |
// TXT message | |
struct txt_message_t : public message_base_t | |
{ | |
void config(bool _hexdump) | |
{ | |
hexdump = _hexdump; | |
} | |
// Create TXT messages with the given "time." | |
void generate_message(const tc::tc_time_t& time, std::vector<uint8_t>& msg) | |
{ | |
std::string str; | |
str = fmt::format("{}-{:02}-{:02} {:02}:{:02}:{:02}.{:03}", | |
time.local_time.tm_year+1900, | |
time.local_time.tm_mon+1, time.local_time.tm_mday, | |
time.local_time.tm_hour, time.local_time.tm_min, | |
time.local_time.tm_sec, time.ms); | |
#if !defined(_WIN32) | |
str += fmt::format(" (frequency drift: {:.3f} ppm)", freq_drift()); | |
#endif | |
str += "\x0d\x0a"; | |
copy(str.begin(), str.end(), back_inserter(msg)); | |
} | |
// Print the received messages in the specified plain or hex formats. | |
void print_received(const std::vector<uint8_t>& buf) | |
{ | |
if(!buf.empty()){ | |
std::cout << fg::green; | |
for(auto c : buf){ | |
if(!hexdump){ | |
std::cout << c << std::flush; | |
}else{ | |
hex_rx.update(c); | |
} | |
} | |
}else{ | |
// Drain the buffer when an RX timeout occurs (detected as a buffer goes empty.) | |
if(!hex_rx.empty()){ | |
std::cout << fg::green; | |
hex_rx.final(); | |
} | |
} | |
} | |
protected: | |
bool hexdump; | |
hex_print_t hex_rx; | |
// Check the frequency drift (the stability of the system clock) using "ntp_adjtime." | |
double freq_drift() | |
{ | |
double ppm = 0; | |
#if !defined(_WIN32) && !defined(WITHOUT_NTP_ADJTIME) | |
// Clear "ntpx.modes" to allow ntp_adjtime() working in read-only mode. | |
struct timex ntpx; | |
ntpx.modes = 0; | |
int status = ntp_adjtime(&ntpx); | |
if((status!=TIME_ERROR) && (ntpx.freq!=0)){ | |
ppm = double(ntpx.freq) / 65536; | |
} | |
#endif | |
return ppm; | |
}; | |
}; | |
// QTR message | |
struct qtr_message_t : public message_base_t | |
{ | |
void config(bool _hexdump = true, uint8_t prefix = 0x3c, uint8_t id = 0x55, | |
uint8_t preamble_len = 10) | |
{ | |
qtr.init(prefix, id, preamble_len); | |
hexdump = _hexdump; | |
} | |
// Create QTR messages with the given "time." | |
void generate_message(const tc::tc_time_t& time, std::vector<uint8_t>& msg) | |
{ | |
qtr.import_time(time.local_time); | |
size_t i = 0; | |
do{ | |
uint8_t c; | |
qtr.encoder_update(c); | |
msg.push_back(c); | |
}while(++i < qtr.num_enc_symbols()); | |
} | |
// Print the decoded messages and the milliseconds of transmission latency. | |
void print_received(const std::vector<uint8_t>& buf, double frame_duration) | |
{ | |
if(buf.empty()) return; | |
if(hexdump){ | |
std::cout << fg::green; | |
for(auto c : buf) hex_rx.update(c); | |
} | |
for(auto c : buf){ | |
if(qtr.decoder_update(c) == true){ | |
if(hexdump){ | |
std::cout << fg::green; | |
hex_rx.final(); | |
}; | |
print_result(frame_duration); | |
} | |
} | |
} | |
protected: | |
qtr_time_sync_t qtr; | |
uint8_t *qtr_message; | |
bool hexdump; | |
hex_print_t hex_rx; | |
void print_result(double frame_duration) | |
{ | |
using namespace std::chrono; | |
static const char* wd[] = { | |
"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" | |
}; | |
auto toa = system_clock::now(); | |
struct TM qtr_time; | |
if(qtr.export_time(qtr_time)){ | |
// Measure the transmission latency and the TX duration using the captured "toa." | |
double tx_duration_ms = duration_cast<microseconds>(toa.time_since_epoch()).count() - | |
1000000.0*timelocal64(&qtr_time); | |
tx_duration_ms /= 1000; | |
double tx_latency_ms = tx_duration_ms - 1000.0*frame_duration*qtr.num_enc_symbols(); | |
std::cout << fg::gray << "Decoded QTR message: " << fg::yellow | |
<< fmt::format("{}-{:02}-{:02} {:02}:{:02}:{:02}, {}. ({:.1f} in {:.1f} ms)", | |
qtr_time.tm_year+1900, qtr_time.tm_mon+1, qtr_time.tm_mday, | |
qtr_time.tm_hour, qtr_time.tm_min, qtr_time.tm_sec, | |
wd[qtr_time.tm_wday % 8], tx_latency_ms, tx_duration_ms) | |
<< std::endl; | |
}else{ | |
perr("Invalid CRC."); | |
} | |
} | |
}; | |
// TOD message | |
struct tod_message_t : public message_base_t | |
{ | |
void config(bool _hexdump = true, const std::string& prefix = "GP") | |
{ | |
tod_prefix = prefix; | |
hexdump = _hexdump; | |
} | |
// Create TOD messages with the given UTC. | |
void generate_message(const tc::tc_time_t& time, std::vector<uint8_t>& msg) | |
{ | |
// $<[-GPZDA] [GNZDA]>,hhmmss.ss,DD,MM,YYYY,aa,AA*hh<CR><LF> | |
std::string time_str = fmt::format( | |
"${}ZDA,{:02}{:02}{:02}.{:02},{:02},{:02},{:04d},{:02},{:02}", | |
tod_prefix, | |
time.utc.tm_hour, time.utc.tm_min, time.utc.tm_sec, time.ms / 10, | |
time.utc.tm_mday, time.utc.tm_mon+1, time.utc.tm_year+1900, 0, 0); | |
// Calculate the checksum using "time_str" to complete the sentence. | |
uint8_t checksum = 0; | |
for(auto c : time_str.substr(1)) checksum ^= c; | |
time_str += fmt::format("*{:02X}\x0d\x0a", checksum); | |
copy(time_str.begin(), time_str.end(), back_inserter(msg)); | |
} | |
// Print the decoded messages and the milliseconds of transmission latency. | |
void print_received(const std::vector<uint8_t>& buf, double frame_duration) | |
{ | |
if(buf.empty()) return; | |
if(hexdump){ | |
std::cout << fg::green; | |
for(auto c : buf) hex_rx.update(c); | |
} | |
if(tod.update(buf)){ | |
if(hexdump){ | |
std::cout << fg::green; | |
hex_rx.final(); | |
} | |
for(size_t i = 0; i < tod.sentences.size(); ++i){ | |
decode_and_print(tod.sentences[i], frame_duration); | |
} | |
} | |
} | |
protected: | |
nmea_sentence_t tod; | |
std::string tod_prefix; | |
bool hexdump; | |
hex_print_t hex_rx; | |
void decode_and_print(const nmea_sentence_t::sentence_t& sentence, double frame_duration) | |
{ | |
using namespace std::chrono; | |
auto toa = system_clock::now(); | |
// Drop any sentences that do not start with the command "ZDA." | |
if(sentence.command.substr(2, 3) != "ZDA"){ | |
return; | |
} | |
// Drop any TOD messages that are likely truncated somewhere. | |
if(sentence.fields.size() < 4){ | |
perr("Invalid TOD message."); | |
return; | |
} | |
// Verify the checksum. | |
if(sentence.checksum_orig != sentence.checksum){ | |
perr("Invalid checksum."); | |
return; | |
}else if(sentence.checksum_orig == -1){ | |
perr(fmt::format("The checksum is missing. Could it be {:02X}?", sentence.checksum)); | |
} | |
// Check whether the received prefix is the same as the specified one. | |
std::string prefix = sentence.command.substr(0, 2); | |
if(prefix != tod_prefix){ | |
perr(fmt::format("The received prefix {} doesn't match the specified {}.", | |
prefix, tod_prefix)); | |
} | |
// Retrieve the "TOD time" from the received message. | |
struct TM tod_time; | |
memset(&tod_time, 0, sizeof(tod_time)); | |
int tod_ms = 0, tod_tz_hour = 0, tod_tz_min = 0; | |
size_t i; | |
for(i = 0; i < sentence.fields.size(); ++i){ | |
std::string str = sentence.fields[i]; | |
try{ | |
switch(i){ | |
case 0: // UTC, hhmmss.ss | |
tod_time.tm_hour = stoi(str.substr(0, 2)); | |
tod_time.tm_min = stoi(str.substr(2, 2)); | |
tod_time.tm_sec = stoi(str.substr(4, 2)); | |
tod_ms = stoi(str.substr(7, 3)) * 10; | |
break; | |
case 1: // Day, 01 to 31 | |
tod_time.tm_mday = stoi(str); | |
break; | |
case 2: // Month, 01 to 12 | |
tod_time.tm_mon = stoi(str) - 1; | |
break; | |
case 3: // Year, yyyy | |
tod_time.tm_year = stoi(str) - 1900; | |
break; | |
case 4: // Local time zone, ranging -13 ~ 13 hours | |
tod_tz_hour = stoi(str); | |
break; | |
case 5: // Local time zone, ranging 00 ~ 59 minutes | |
tod_tz_min = stoi(str); | |
break; | |
default: | |
i = sentence.fields.size(); | |
break; | |
} | |
}catch(std::exception& e){ | |
perr(fmt::format("The received message contains an invalid field {} -- {}", | |
str, e.what())); | |
return; | |
} | |
} | |
// Print any problems that possibly remain. | |
if(i < 6){ | |
perr(fmt::format("Field No. {} is too short to represent the time zone.", i)); | |
tod_tz_hour = 0; | |
tod_tz_min = 0; | |
}else if(i > 6){ | |
perr(fmt::format("This message contains {} field(s) that are ignored from parsing.", | |
i-6)); | |
} | |
// Measure the transmission latency and the TX duration using the captured "toa." | |
double tx_duration_ms = duration_cast<microseconds>(toa.time_since_epoch()).count() - | |
(1000000.0*timegm64(&tod_time) + 1000.0*tod_ms); | |
tx_duration_ms /= 1000; | |
double tx_latency_ms = tx_duration_ms - 1000.0*frame_duration*sentence.nbytes; | |
std::cout << fg::gray << fmt::format("Decoded {}ZDA: ", sentence.command.substr(0, 2)) | |
<< fg::yellow | |
<< fmt::format("{}-{:02}-{:02} {:02}:{:02}:{:02}.{}, " | |
"tz = {} h {} m. ({:.1f} in {:.1f} ms)", | |
tod_time.tm_year+1900, tod_time.tm_mon+1, tod_time.tm_mday, | |
tod_time.tm_hour, tod_time.tm_min, tod_time.tm_sec, | |
tod_ms, tod_tz_hour, tod_tz_min, tx_latency_ms, tx_duration_ms) | |
<< std::endl; | |
} | |
}; | |
// ------------------------------------------------------------------------------------------------ | |
// This type handles the options and arguments in the command line. | |
struct cmdline_t | |
{ | |
bool op_rx = true, op_tx = false; | |
enum class msg_type_t : uint8_t {TXT, TOD, QTR} msg_type = msg_type_t::TXT; | |
std::string tod_prefix = "GP"; | |
int16_t qtr_m = 60, qtr_n = 0, qtr_p = 60; | |
int64_t timer_offset_ms = 0; | |
bool hexdump = false; | |
std::string port_name; | |
uint32_t baud_rate; | |
uint8_t dbits = 8, sbits = 1; | |
serial_port_t::parity_t parity = serial_port_t::parity_t::SP_PARITY_NONE; | |
bool usage_printed = false; | |
bool get_config(int argc, char* argv[]) | |
{ | |
static const struct option long_options[]={ | |
{"help", no_argument, nullptr, 711}, // for comments | |
{"operation", optional_argument, nullptr, 'o'}, | |
{"QTR", optional_argument, nullptr, 'q'}, | |
{"TOD", optional_argument, nullptr, 't'}, | |
{"advance", optional_argument, nullptr, 'a'}, | |
{"hex", no_argument, nullptr, 'x'}, | |
{nullptr, 0, nullptr, 0} | |
}; | |
static const char* short_options = "ho::q::t::a::x"; | |
// Find the options. | |
bool option_parsed = false; | |
while(1){ | |
int c = getopt_long(argc, argv, short_options, long_options, nullptr); | |
if(c == -1) break; | |
switch(c){ | |
case 0: | |
// Code should only get here if a long option has a non-null flag value. | |
return false; | |
case '?': | |
// getopt_long has already printed its error messages. | |
return false; | |
default: | |
option_parsed = false; | |
dispatcher_options.dispatch(c, optarg, option_parsed); | |
if(!option_parsed) return false; | |
} | |
} | |
// Find the arguments. | |
size_t nargs = 0; | |
while(optind < argc){ | |
if(nargs <= 4){ | |
bool arg_parsed = false; | |
dispatcher_args.dispatch(nargs, argv[optind], arg_parsed); | |
if(!arg_parsed) return false; | |
}else{ | |
perr("Too many parameters."); | |
return false; | |
} | |
optind++; | |
nargs++; | |
} | |
// nargs==0 is allowed to enumerate the serial ports from the system. | |
if((nargs==1) || (nargs==0) & option_parsed){ | |
perr("Too few parameters."); | |
return false; | |
} | |
return true; | |
} | |
static void perr(const std::string& msg) | |
{ | |
std::cerr << style::bold << fgB::magenta << msg << style::reset << std::endl; | |
} | |
// The constructor. | |
cmdline_t() | |
{ | |
// "-h" - print the help | |
dispatcher_options.appendListener('h', [&](const char*, bool&) | |
{ | |
std::cout << style::reset << help_messages::usage() << std::flush; | |
usage_printed = true; | |
}); | |
// 711 - print comments | |
dispatcher_options.appendListener(711, [&](const char*, bool&) | |
{ | |
std::cout << fg::green << help_messages::usage() << fg::yellow | |
<< help_messages::comments() << style::reset << std::flush; | |
usage_printed = true; | |
}); | |
// "-o" - the operating mode | |
dispatcher_options.appendListener('o', [&](const char* opt, bool& parsed) | |
{ | |
if(opt == nullptr){ | |
parsed = true; | |
return; | |
} | |
if(first_hyphen_o){ | |
first_hyphen_o = false; | |
op_tx = op_rx = false; | |
} | |
if(!strcasecmp(opt, "r")){ | |
op_rx = true; | |
}else if(!strcasecmp(opt, "t")){ | |
op_tx = true; | |
}else if((!strcasecmp(opt, "rt")) || (!strcasecmp(opt, "tr"))){ | |
op_tx = op_rx = true; | |
}else{ | |
perr(fmt::format("Invalid operating mode: {}.", opt)); | |
return; | |
} | |
parsed = true; | |
}); | |
// "-q" - the QTR type | |
dispatcher_options.appendListener('q', [&](const char* opt, bool& parsed) | |
{ | |
if((msg_type!=msg_type_t::TXT) && (msg_type!=msg_type_t::QTR)){ | |
perr("Cannot enable QTR, another message type has been specified."); | |
return; | |
}else{ | |
msg_type = msg_type_t::QTR; | |
} | |
if(opt == nullptr){ | |
parsed = true; | |
return; | |
} | |
// Find the sub-arguments m, n, and p values from the prefix "-q." | |
std::vector<int> args = {qtr_m, qtr_n, qtr_p}; | |
std::vector<char> mnp = {'m', 'n', 'p'}; | |
std::vector<std::string> token = split_str(opt, ",@"); | |
if(token.size() > mnp.size()){ | |
perr(fmt::format("Invalid format of the QTR argument: {}.", opt)); | |
return; | |
} | |
for(size_t i = 0; i < token.size(); ++i){ | |
if(token[i] != ""){ | |
size_t endp; | |
int tmp; | |
try{ | |
tmp = stoi(token[i], &endp); | |
}catch(std::exception& e){ | |
perr(fmt::format("The QTR sub-argument {:c} must be a number -- {}", | |
mnp[i], e.what())); | |
return; | |
} | |
if(endp != token[i].size()){ | |
perr(fmt::format("The QTR sub-argument {:c} is invalid -- {}", | |
mnp[i], token[i])); | |
return; | |
}else{ | |
args[i] = tmp; | |
} | |
} | |
} | |
if((args[2]<1) || (args[2]>3600)){ | |
perr(fmt::format("The QTR sub-argument p is out of range -- {} ({},{},{})", | |
args[2], args[0], args[1], args[2])); | |
return; | |
}else if(args[1] > (args[2]-1)){ | |
perr(fmt::format("The QTR sub-argument n is out of range -- {} ({},{},{})", | |
args[1], args[0], args[1], args[2])); | |
return; | |
}else if((args[0]<1) || (args[0]>args[2])){ | |
perr(fmt::format("The QTR sub-argument m is out of range -- {} ({},{},{})", | |
args[0], args[0], args[1], args[2])); | |
return; | |
} | |
qtr_m = args[0], qtr_n = args[1], qtr_p = args[2]; | |
parsed = true; | |
}); | |
// "-t" - the TOD type | |
dispatcher_options.appendListener('t', [&](const char* opt, bool& parsed) | |
{ | |
if((msg_type!=msg_type_t::TXT) && (msg_type!=msg_type_t::TOD)){ | |
perr("Cannot enable TOD, another message type has been specified."); | |
return; | |
}else{ | |
msg_type = msg_type_t::TOD; | |
} | |
if(opt == nullptr){ | |
parsed = true; | |
return; | |
} | |
tod_prefix = opt; | |
for(auto &c: tod_prefix){ | |
if(!isalpha(c)){ | |
perr(fmt::format("Non-alphabet TOD prefix: {}.", opt)); | |
return; | |
} | |
c = static_cast <char> (toupper(c)); | |
} | |
if(tod_prefix.size() != 2){ | |
perr(fmt::format("The length of the TOD prefix is invalid -- {}.", opt)); | |
return; | |
} | |
parsed = true; | |
}); | |
// "-a" - time offset for transmitting the messages | |
dispatcher_options.appendListener('a', [&](const char* opt, bool& parsed) | |
{ | |
if(opt == nullptr){ | |
parsed = true; | |
return; | |
} | |
char* endp; | |
long long tmp = strtoll(opt, &endp, 10); | |
if((opt==endp) || (*endp!=0)){ | |
perr(fmt::format("The milliseconds value is invalid -- {}.", opt)); | |
return; | |
}else{ | |
timer_offset_ms = int64_t(tmp); | |
} | |
parsed = true; | |
}); | |
// "-x" - the HEX printing | |
dispatcher_options.appendListener('x', [&](const char*, bool& parsed) | |
{ | |
hexdump = true; | |
parsed = true; | |
}); | |
// // argument 0 - the port name | |
dispatcher_args.appendListener(0, [&](const char* arg, bool& parsed) | |
{ | |
port_name = arg; | |
#if defined(_WIN32) | |
for(auto& c: port_name) c = static_cast<char>(toupper(c)); | |
#endif | |
parsed = true; | |
}); | |
// argument 1 - the Baud rate | |
dispatcher_args.appendListener(1, [&](const char* arg, bool& parsed) | |
{ | |
char* endp; | |
long long tmp = strtoll(arg, &endp, 10); | |
if((arg==endp) || (*endp!=0)){ | |
perr(fmt::format("The Baud rate {} is invalid.", arg)); | |
return; | |
}else if((tmp<1) || (tmp>INT32_MAX)){ | |
perr(fmt::format("The Baud rate {} is out of range.", arg)); | |
return; | |
}else{ | |
baud_rate = uint32_t(tmp); | |
} | |
parsed = true; | |
}); | |
// argument 2 - the data bits | |
dispatcher_args.appendListener(2, [&](const char* arg, bool& parsed) | |
{ | |
char* endp; | |
long long tmp = strtoll(arg, &endp, 10); | |
if((arg==endp) || (*endp!=0)){ | |
perr(fmt::format("The value of data bits is invalid -- {}.", arg)); | |
return; | |
}else if((tmp<5) || (tmp>8)){ | |
perr(fmt::format("The length of the data frame is invalid -- {}.", arg)); | |
return; | |
}else{ | |
dbits = uint8_t(tmp); | |
} | |
parsed = true; | |
}); | |
// argument 3 - the parity type | |
dispatcher_args.appendListener(3, [&](const char* arg, bool& parsed) | |
{ | |
if(!strcasecmp(arg, "e")){ | |
parity = serial_port_t::parity_t::SP_PARITY_EVEN; | |
}else if(!strcasecmp(arg, "o")){ | |
parity = serial_port_t::parity_t::SP_PARITY_ODD; | |
}else if(!strcasecmp(arg, "n")){ | |
parity = serial_port_t::parity_t::SP_PARITY_NONE; | |
}else{ | |
/* The features of SP_PARITY "mark" and "space" have not been implemented. Those | |
* are dedicated to bus-oriented MCU networks and used to identify node addresses. | |
*/ | |
perr(fmt::format("The parity type {} is invalid.", arg)); | |
return; | |
} | |
parsed = true; | |
}); | |
// argument 4 - the stop bits | |
dispatcher_args.appendListener(4, [&](const char* arg, bool& parsed) | |
{ | |
char* endp; | |
long long tmp = strtoll(arg, &endp, 10); | |
if((arg==endp) || (*endp!=0)){ | |
perr(fmt::format("The value of stop bits is invalid -- {}.", arg)); | |
return; | |
}else if((tmp<1) || (tmp>2)){ | |
perr(fmt::format("The number specified for stop bits is invalid -- {}.", arg)); | |
return; | |
}else{ | |
sbits = uint8_t(tmp); | |
} | |
parsed = true; | |
}); | |
} | |
private: | |
bool first_hyphen_o = true; | |
eventpp::EventDispatcher<int, void(const char*, bool&)> dispatcher_options; | |
eventpp::EventDispatcher<int, void(const char*, bool&)> dispatcher_args; | |
std::vector<std::string> split_str(const std::string& str, const std::string& delim) | |
{ | |
std::vector<std::string> result; | |
if(str.empty()) return result; | |
std::string::const_iterator begin, iter; | |
begin = iter = str.begin(); | |
do{ | |
// Find the initial position of the given delimiter from the iterating "iter." | |
while((delim.find(*iter)==std::string::npos) && (iter<str.end())) iter++; | |
// Save the result as a token. | |
result.push_back(std::string(begin, iter++)); | |
begin = iter; | |
}while(begin < str.end()); | |
return result; | |
} | |
}; | |
// ------------------------------------------------------------------------------------------------ | |
// Serial port operation | |
struct serial_op_t | |
{ | |
serial_op_t(const cmdline_t& command_line_conf) : conf(command_line_conf) | |
{ | |
// Events for TXT messages. | |
txt.config(conf.hexdump); | |
tx_events.appendListener(cmdline_t::msg_type_t::TXT, [&](const tc::tc_time_t& time) | |
{ | |
std::vector<uint8_t> msg; | |
txt.generate_message(time, msg); | |
port.write(msg, TX_TIMEOUT_MS); | |
std::cout << fg::gray; | |
txt.print_text(msg); | |
std::cout << std::endl; | |
}); | |
rx_events.appendListener(cmdline_t::msg_type_t::TXT, [&]() | |
{ | |
while(1){ | |
uint8_t buf[256]; | |
size_t nread = port.read(buf, sizeof buf, RX_TIMEOUT_MS); | |
txt.print_received(std::vector<uint8_t>(buf, buf+nread)); | |
} | |
}); | |
// Events for QTR messages. | |
qtr.config(conf.hexdump); | |
qtr_tx_second = conf.qtr_m == conf.qtr_p ? 0 : conf.qtr_m; | |
tx_events.appendListener(cmdline_t::msg_type_t::QTR, [&](const tc::tc_time_t& time) | |
{ | |
auto send_qtr_message = [&](const tc::tc_time_t& time) | |
{ | |
std::vector<uint8_t> msg; | |
qtr.generate_message(time, msg); | |
port.write(msg, TX_TIMEOUT_MS); | |
std::cout << fg::gray; | |
qtr.print_hex(msg); | |
}; | |
int16_t sec = (time.local_time.tm_min*60 + time.local_time.tm_sec) % conf.qtr_p; | |
if(sec == conf.qtr_n){ | |
qtr_tx_second = 0; | |
} | |
if(qtr_tx_second < conf.qtr_m){ | |
send_qtr_message(time); | |
qtr_tx_second++; | |
} | |
}); | |
rx_events.appendListener(cmdline_t::msg_type_t::QTR, [&]() | |
{ | |
while(1){ | |
uint8_t buf[256]; | |
size_t nread = port.read(buf, sizeof buf, RX_TIMEOUT_MS); | |
qtr.print_received(std::vector<uint8_t>(buf, buf+nread), port.frame_duration()); | |
} | |
}); | |
// Events for TOD messages. | |
tod.config(conf.hexdump, conf.tod_prefix); | |
tx_events.appendListener(cmdline_t::msg_type_t::TOD, [&](const tc::tc_time_t& time) | |
{ | |
std::vector<uint8_t> msg; | |
tod.generate_message(time, msg); | |
port.write(msg, TX_TIMEOUT_MS); | |
std::cout << fg::gray; | |
tod.print_text(msg); | |
std::cout << std::endl; | |
}); | |
rx_events.appendListener(cmdline_t::msg_type_t::TOD, [&]() | |
{ | |
while(1){ | |
uint8_t buf[256]; | |
size_t nread = port.read(buf, sizeof buf, RX_TIMEOUT_MS); | |
tod.print_received(std::vector<uint8_t>(buf, buf+nread), port.frame_duration()); | |
} | |
}); | |
// Create a periodical timer for TX. | |
timer.set_time_offset(conf.timer_offset_ms); | |
timer.callbacks.append([&](const tc::tc_time_t& time, bool timing_ok) | |
{ | |
tx_events.dispatch(conf.msg_type, time); | |
if(!timing_ok){ | |
message_base_t::perr("A timing glitch might have occurred."); | |
} | |
}); | |
} | |
int run() | |
{ | |
using namespace std::chrono; | |
try{ | |
port.open(conf.port_name, conf.baud_rate, conf.dbits, conf.parity, conf.sbits); | |
}catch(std::exception& e){ | |
message_base_t::perr(std::string(e.what())); | |
return -1; | |
} | |
print_port_config(); | |
std::cout << fg::cyan << std::flush; | |
if(conf.op_tx){ | |
timer.start(TIMER_READ_INTERVAL); | |
std::cout << "The TX is enabled"; | |
if(conf.msg_type == cmdline_t::msg_type_t::QTR){ | |
std::cout << fmt::format(" to handle the QTR messages around \"{},{},{}\"", | |
int(conf.qtr_m), int(conf.qtr_n), int(conf.qtr_p)); | |
} | |
std::cout << "." << std::endl; | |
} | |
if(conf.op_rx){ | |
std::cout << "The RX is enabled"; | |
if(conf.hexdump){ | |
std::cout << " with hexdumps"; | |
} | |
std::cout << "." << std::endl; | |
rx_events.dispatch(conf.msg_type); | |
} | |
if(conf.op_tx | conf.op_rx){ | |
while(1) std::this_thread::sleep_for(hours(24)); | |
}else{ | |
message_base_t::perr("Neither a TX nor RX was enabled. Check the code!"); | |
return -1; | |
} | |
return 0; | |
} | |
private: | |
static const uint8_t RX_TIMEOUT_MS = 100; | |
static const uint8_t TX_TIMEOUT_MS = 100; | |
static const int TIMER_READ_INTERVAL = 1000; | |
eventpp::EventDispatcher<cmdline_t::msg_type_t, void()> rx_events; | |
eventpp::EventDispatcher<cmdline_t::msg_type_t, void(const tc::tc_time_t&)> tx_events; | |
const cmdline_t& conf; | |
serial_port_t port; | |
tc timer; | |
txt_message_t txt; | |
qtr_message_t qtr; | |
tod_message_t tod; | |
// This counter controls the duty cycle of sending the QTR messages. | |
int16_t qtr_tx_second; | |
void print_port_config() | |
{ | |
// Get the port name. | |
std::string s = port.get_port_name(); | |
// Get the basic configuration. | |
int baudrate = 0, bits = 0, stopbits = 0; | |
serial_port_t::parity_t parity; | |
port.get_basic_config(baudrate, bits, parity, stopbits); | |
std::cout << fg::magenta | |
<< fmt::format("{} is open with \"{}, {}, {}, {}\"", s, baudrate, bits, | |
port.get_parity_string(parity), stopbits); | |
// Get the description. | |
std::string description; | |
if((description=port.get_port_description()) != ""){ | |
std::cout << fmt::format(" as {}", description); | |
} | |
std::cout << "." << std::endl; | |
// Find the transport that the port gets connected through. | |
serial_port_t::transport_t transport = port.get_port_transport(); | |
switch(transport){ | |
case serial_port_t::transport_t::SP_TRANSPORT_NATIVE: | |
// The "native" below depends on how the system recognizes a connected port. | |
std::cout << "It's a system identified native UART." << std::endl; | |
break; | |
case serial_port_t::transport_t::SP_TRANSPORT_USB: { | |
std::cout << "It's a USB UART"; | |
int bus, address, vid, pid; | |
if(port.get_usb_bus_address(bus, address)){ | |
std::cout << fmt::format(": bus = {}, address = {}", bus, address); | |
} | |
if(port.get_usb_vid_pid(vid, pid)){ | |
std::cout << fmt::format(", VID = {}, PID = {}", vid, pid); | |
} | |
std::cout << "." << std::endl; | |
if((s=port.get_usb_product_name()) != ""){ | |
if(description.find(s) == std::string::npos){ | |
std::cout << fmt::format("The product name is {}.", s) << std::endl; | |
} | |
} | |
if((s=port.get_usb_serial_number()) != ""){ | |
if(description.find(s) == std::string::npos){ | |
std::cout << fmt::format("The serial number is {}.", s) << std::endl; | |
} | |
} | |
if((s=port.get_usb_manufacturer()) != ""){ | |
if(description.find(s) == std::string::npos){ | |
std::cout << fmt::format("The manufactor is {}.", s) << std::endl; | |
} | |
} | |
} | |
break; | |
case serial_port_t::transport_t::SP_TRANSPORT_BLUETOOTH: | |
std::cout << "It's a Bluetooth UART"; | |
if((s=port.get_bluetooth_address()) != ""){ | |
std::cout << fmt::format(": MAC = {}", s); | |
} | |
std::cout << "." << std::endl; | |
break; | |
} | |
} | |
}; | |
// ------------------------------------------------------------------------------------------------ | |
// Enumerate serial ports attached to the system. | |
inline void enumerate_serial_ports() | |
{ | |
using namespace std::chrono; | |
// The sp function sp_list_ports() is used here. | |
auto start = high_resolution_clock::now(); | |
struct sp_port **port_list; | |
if(sp_list_ports(&port_list) == SP_OK){ | |
duration<double> elapsed = high_resolution_clock::now() - start; | |
// Print the port name and the description of an enumerated port. | |
size_t port_index = 0; | |
if(port_list[port_index]){ | |
std::cout << style::italic | |
<< "Serial port(s)" << std::endl; | |
while(port_list[port_index]){ | |
if(char* s = sp_get_port_name(port_list[port_index])){ | |
std::cout << fg::green << s; | |
}else{ | |
throw std::runtime_error(fmt::format("{}(): Port {} lacks its name.", | |
__func__, port_index)); | |
} | |
if(char* s = sp_get_port_description(port_list[port_index])){ | |
std::cout << fg::yellow << fmt::format(" ({})", s); | |
} | |
std::cout << std::endl; | |
port_index++; | |
} | |
// This duration indicates how long the enumeration took. It's mainly for XP and 2000. | |
std::cout << fg::gray << fmt::format("(discovered in {:.3f} seconds.)", | |
elapsed.count()) | |
<< style::reset << std::endl; | |
}else{ | |
cmdline_t::perr("No serial ports found."); | |
} | |
}else{ | |
cmdline_t::perr("Error enumerating the serial ports on this system."); | |
} | |
// Free the space the sp_list_ports() allocated. | |
sp_free_port_list(port_list); | |
} | |
inline int get_time_offset() | |
{ | |
time_t gmt, rawtime = time(NULL); | |
struct tm *ptm; | |
#if !defined(WIN32) | |
struct tm gbuf; | |
ptm = gmtime_r(&rawtime, &gbuf); | |
#else | |
ptm = gmtime(&rawtime); | |
#endif | |
// Access the time zone database to check the DST. | |
ptm->tm_isdst = -1; | |
gmt = mktime(ptm); | |
return int(difftime(rawtime, gmt)); | |
} | |
static void signal_handler(int sig_num) | |
{ | |
signal(sig_num, signal_handler); | |
#if !defined(_WIN32) | |
if(sig_num == SIGCHLD){ | |
do{} while (waitpid(-1, &sig_num, WNOHANG) > 0); | |
}else | |
#endif | |
{ | |
// Reset the display style and exit. | |
std::cout << style::reset << "signal " << sig_num << std::endl; | |
exit(sig_num); | |
} | |
} | |
int main(int argc, char* argv[]) | |
{ | |
// Set the handlers that are needed to respond to the exit signals. | |
signal(SIGTERM, signal_handler); | |
signal(SIGINT, signal_handler); | |
signal(SIGABRT, signal_handler); | |
#if !defined(_WIN32) | |
signal(SIGCHLD, signal_handler); | |
#endif | |
std::cout << style::reset << help_messages::version_info() << std::flush; | |
cmdline_t cline; | |
if(!cline.get_config(argc, argv)){ | |
if(!cline.usage_printed) cline.perr("Type time2tty -h or --help for helps."); | |
return -1; | |
} | |
if(cline.port_name.empty()){ | |
int gm_offset = get_time_offset(); | |
std::cout << style::bold << fg::blue << help_messages::libraries_used() | |
<< fmt::format("The local time differs from UTC by {} seconds ({:.1f} hours.)\n", | |
gm_offset, gm_offset/3600.0) | |
<< std::endl; | |
enumerate_serial_ports(); | |
return -1; | |
} | |
serial_op_t sop(cline); | |
return sop.run(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* time2tty.h | |
* | |
* This file is part of the QTR Clock AVR, my project for displaying time using 8051 or AVR. | |
* | |
* time2tty, version 1.0 Copyright (C) 2010-2023 BD1ES. | |
* | |
* This program is free software: you can redistribute it and/or modify | |
* it under the terms of the GNU General Public License as published by | |
* the Free Software Foundation, either version 3 of the License, or | |
* (at your option) any later version. | |
* | |
* This program is distributed in the hope that it will be useful, | |
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
* GNU General Public License for more details. | |
* | |
* You should have received a copy of the GNU General Public License | |
* along with this program. If not, see <https://www.gnu.org/licenses/>. | |
*/ | |
#ifndef TIME2TTY_H | |
#define TIME2TTY_H | |
#include <getopt.h> | |
#include <iostream> | |
#include <string> | |
#include <vector> | |
#include <chrono> | |
#include <thread> | |
#if !defined(_WIN32) | |
#include <signal.h> | |
#include <sys/wait.h> | |
#include <sys/timex.h> | |
#endif | |
#include <fmt/format.h> | |
#include <libserialport.h> | |
#include <time64.h> | |
#include <eventpp/eventqueue.h> | |
// These macros are defined for sharing AVR GCC code with PC programs. | |
#if defined(__GNUC__) && defined(__AVR_ARCH__) | |
#include <avr/pgmspace.h> | |
#define PGM_ROM_SPACE const PROGMEM | |
#else | |
#define PGM_ROM_SPACE const | |
#define pgm_read_byte * | |
#define pgm_read_word * | |
#define pgm_read_dword * | |
#endif | |
// ------------------------------------------------------------------------------------------------ | |
/* This type is from the QTR clock code, which defines the "type 1" message that transfers "time" | |
* from the master clock to listeners with the same transmission parameters. | |
*/ | |
struct qtr_time_sync_t | |
{ | |
qtr_time_sync_t() | |
{ | |
// Enable state machines to handle Hamming encoding and decoding. | |
asy.enc_state = asy.dec_state = PREFIX; | |
asy.enc_ptr = asy.dec_ptr = 0; | |
asy.enc_byte = asy.dec_byte = 0; | |
asy.enc_byte_complete = asy.dec_byte_complete = true; | |
// This is the default channel parameters. | |
init(0x3c, 0x55, 10); | |
} | |
// Set with the user-specified parameters. | |
void init(uint8_t prefix, uint8_t id, uint8_t prefix_seqlen) | |
{ | |
asy.prefix = prefix; | |
asy.id = id; | |
asy.prefix_seqlen = prefix_seqlen; | |
/* This interleaved pattern is used to determine the symbol loss in reception. If | |
* the transmission condition is suboptimal, the UART may fail to detect start or | |
* stop bits, causing frame loss. | |
*/ | |
asy.prefix_garpat = uint8_t(asy.prefix<<4) | uint8_t(asy.prefix>>4); | |
} | |
#if (defined(__GNUC__) && defined(__AVR_ARCH__)) || defined(DAC_CDAC_APPS) | |
void import_time(const qdt_gd_t& time_in) | |
{ | |
// Compose the message with the given time. | |
asy.enc_message[0] = time_in.second; | |
asy.enc_message[1] = time_in.minute; | |
asy.enc_message[2] = time_in.hour; | |
asy.enc_message[3] = time_in.day; | |
asy.enc_message[4] = time_in.month; | |
asy.enc_message[5] = uint8_t(time_in.year / 256); | |
asy.enc_message[6] = uint8_t(time_in.year % 256); | |
asy.enc_message[7] = time_in.weekday; | |
// Calculate the CRC. | |
uint16_t crc = calc_crc16(asy.enc_message, 8); | |
asy.enc_message[8] = crc / 256; | |
asy.enc_message[9] = uint8_t(crc % 256); | |
} | |
/* This function returns true if the received CRC is verified. It may not be placed in an ISR | |
* because the Hamming processing (function decrypt_update) may take milliseconds to complete, | |
* and the MCU's ISR may need a much quicker response to the next interrupt. | |
*/ | |
bool export_time(qdt_gd_t& atm) | |
{ | |
uint16_t crc = calc_crc16(asy.dec_message, 8); | |
if((asy.dec_message[8]==(crc/256)) && (asy.dec_message[9]==(crc%256))){ | |
atm.second = asy.dec_message[0]; | |
atm.minute = asy.dec_message[1]; | |
atm.hour = asy.dec_message[2]; | |
atm.day = asy.dec_message[3]; | |
atm.month = asy.dec_message[4]; | |
atm.year = asy.dec_message[5]*256 + asy.dec_message[6]; | |
atm.weekday = asy.dec_message[7] % 7; | |
atm.doy = qdt_gd_ordinal_date(static_cast <const qdt_gd_t *> (&atm)); | |
return true; | |
} | |
return false; | |
} | |
#else | |
void import_time(const struct TM& time_in) | |
{ | |
// Compose the message with the given time. | |
asy.enc_message[0] = uint8_t(time_in.tm_sec); | |
asy.enc_message[1] = uint8_t(time_in.tm_min); | |
asy.enc_message[2] = uint8_t(time_in.tm_hour); | |
asy.enc_message[3] = uint8_t(time_in.tm_mday); | |
asy.enc_message[4] = uint8_t(time_in.tm_mon + 1); | |
uint16_t year = uint16_t(time_in.tm_year + 1900); | |
asy.enc_message[5] = year/256; | |
asy.enc_message[6] = uint8_t(year%256); | |
asy.enc_message[7] = uint8_t(time_in.tm_wday); | |
// Calculate the CRC. | |
uint16_t crc = calc_crc16(asy.enc_message, 8); | |
asy.enc_message[8] = crc/256; | |
asy.enc_message[9] = uint8_t(crc%256); | |
} | |
bool export_time(struct TM& atm) | |
{ | |
uint16_t crc = calc_crc16(asy.dec_message, 8); | |
if((asy.dec_message[8]==(crc/256)) && (asy.dec_message[9]==(crc%256))){ | |
memset(&atm, 0, sizeof(atm)); | |
atm.tm_sec = asy.dec_message[0]; | |
atm.tm_min = asy.dec_message[1]; | |
atm.tm_hour = asy.dec_message[2]; | |
atm.tm_mday = asy.dec_message[3]; | |
atm.tm_mon = asy.dec_message[4] - 1; | |
atm.tm_year = asy.dec_message[5]*256 + asy.dec_message[6] - 1900; | |
atm.tm_wday = asy.dec_message[7] % 7; | |
atm.tm_isdst = -1; | |
return true; | |
} | |
return false; | |
} | |
#endif | |
// This function returns true when the encoding iteration is completed. | |
bool encoder_update(uint8_t& symbol_out) | |
{ | |
// [8,4] extended Hamming encoding table: | |
static PGM_ROM_SPACE uint8_t hamming_enc_table[16] = { | |
0x15, 0x02, 0x49, 0x5e, 0x64, 0x73, 0x38, 0x2f, | |
0xd0, 0xc7, 0x8c, 0x9b, 0xa1, 0xb6, 0xfd, 0xea | |
}; | |
// Generate channel symbols using the type 1 message format. | |
if(asy.enc_byte_complete){ | |
switch(asy.enc_state){ | |
case PREFIX: | |
asy.enc_byte = asy.prefix; | |
if(++asy.enc_ptr == asy.prefix_seqlen){ | |
asy.enc_ptr = 0; | |
asy.enc_state = ID; | |
} | |
break; | |
case ID: | |
asy.enc_byte = asy.id; | |
asy.enc_state = MESSAGE; | |
break; | |
case MESSAGE: | |
asy.enc_byte = asy.enc_message[asy.enc_ptr]; | |
if(++asy.enc_ptr == sizeof(asy.enc_message)){ | |
asy.enc_ptr = 0; | |
asy.enc_state = IDLE; | |
} | |
break; | |
case IDLE: | |
break; | |
} | |
} | |
// Split a byte into a pair of 4-bit Hamming symbols. | |
if(asy.enc_byte_complete){ | |
symbol_out = pgm_read_byte(&hamming_enc_table[asy.enc_byte / 16]); | |
asy.enc_byte_complete = false; | |
}else{ | |
symbol_out = pgm_read_byte(&hamming_enc_table[asy.enc_byte % 16]); | |
asy.enc_byte_complete = true; | |
// The encoding is completed. | |
if(asy.enc_state == IDLE){ | |
asy.enc_state = PREFIX; | |
return true; | |
} | |
} | |
// The encoding is iterating. | |
return false; | |
} | |
// This function returns true when the message has been restored. | |
bool decoder_update(uint8_t incoming_symble) | |
{ | |
// [8,4] extended Hamming decoding table: | |
static PGM_ROM_SPACE uint8_t hamming_dec_table[256] = { | |
0x01, 0xff, 0x01, 0x01, 0xff, 0x00, 0x01, 0xff, 0xff, 0x02, 0x01, 0xff, | |
0x0a, 0xff, 0xff, 0x07, 0xff, 0x00, 0x01, 0xff, 0x00, 0x00, 0xff, 0x00, | |
0x06, 0xff, 0xff, 0x0b, 0xff, 0x00, 0x03, 0xff, 0xff, 0x0c, 0x01, 0xff, | |
0x04, 0xff, 0xff, 0x07, 0x06, 0xff, 0xff, 0x07, 0xff, 0x07, 0x07, 0x07, | |
0x06, 0xff, 0xff, 0x05, 0xff, 0x00, 0x0d, 0xff, 0x06, 0x06, 0x06, 0xff, | |
0x06, 0xff, 0xff, 0x07, 0xff, 0x02, 0x01, 0xff, 0x04, 0xff, 0xff, 0x09, | |
0x02, 0x02, 0xff, 0x02, 0xff, 0x02, 0x03, 0xff, 0x08, 0xff, 0xff, 0x05, | |
0xff, 0x00, 0x03, 0xff, 0xff, 0x02, 0x03, 0xff, 0x03, 0xff, 0x03, 0x03, | |
0x04, 0xff, 0xff, 0x05, 0x04, 0x04, 0x04, 0xff, 0xff, 0x02, 0x0f, 0xff, | |
0x04, 0xff, 0xff, 0x07, 0xff, 0x05, 0x05, 0x05, 0x04, 0xff, 0xff, 0x05, | |
0x06, 0xff, 0xff, 0x05, 0xff, 0x0e, 0x03, 0xff, 0xff, 0x0c, 0x01, 0xff, | |
0x0a, 0xff, 0xff, 0x09, 0x0a, 0xff, 0xff, 0x0b, 0x0a, 0x0a, 0x0a, 0xff, | |
0x08, 0xff, 0xff, 0x0b, 0xff, 0x00, 0x0d, 0xff, 0xff, 0x0b, 0x0b, 0x0b, | |
0x0a, 0xff, 0xff, 0x0b, 0x0c, 0x0c, 0xff, 0x0c, 0xff, 0x0c, 0x0d, 0xff, | |
0xff, 0x0c, 0x0f, 0xff, 0x0a, 0xff, 0xff, 0x07, 0xff, 0x0c, 0x0d, 0xff, | |
0x0d, 0xff, 0x0d, 0x0d, 0x06, 0xff, 0xff, 0x0b, 0xff, 0x0e, 0x0d, 0xff, | |
0x08, 0xff, 0xff, 0x09, 0xff, 0x09, 0x09, 0x09, 0xff, 0x02, 0x0f, 0xff, | |
0x0a, 0xff, 0xff, 0x09, 0x08, 0x08, 0x08, 0xff, 0x08, 0xff, 0xff, 0x09, | |
0x08, 0xff, 0xff, 0x0b, 0xff, 0x0e, 0x03, 0xff, 0xff, 0x0c, 0x0f, 0xff, | |
0x04, 0xff, 0xff, 0x09, 0x0f, 0xff, 0x0f, 0x0f, 0xff, 0x0e, 0x0f, 0xff, | |
0x08, 0xff, 0xff, 0x05, 0xff, 0x0e, 0x0d, 0xff, 0xff, 0x0e, 0x0f, 0xff, | |
0x0e, 0x0e, 0xff, 0x0e | |
}; | |
// Get a 4-bit decoding result from the received symbol. | |
if(asy.dec_byte_complete){ | |
uint8_t tmp = pgm_read_byte(&hamming_dec_table[incoming_symble]); | |
asy.dec_byte = tmp * 16; | |
// Mark the pending decoding of the following 4 bits. | |
asy.dec_byte_complete = false; | |
return false; | |
}else{ | |
uint8_t tmp = pgm_read_byte(&hamming_dec_table[incoming_symble]); | |
asy.dec_byte += tmp; | |
asy.dec_byte_complete = true; | |
} | |
// Recover the message using the decoded bytes. | |
switch(asy.dec_state){ | |
case PREFIX: | |
if(asy.dec_byte == asy.prefix){ | |
asy.dec_state = ID; | |
} | |
// Purge the incomplete prefixes. | |
if(asy.dec_byte == asy.prefix_garpat){ | |
asy.dec_byte_complete = false; | |
} | |
break; | |
case ID: | |
if(asy.dec_byte != asy.prefix){ | |
// Bytes received before the ID should be prefix characters. | |
if(asy.dec_byte == asy.id){ | |
asy.dec_state = MESSAGE; | |
}else{ | |
asy.dec_state = PREFIX; | |
} | |
} | |
break; | |
case MESSAGE: | |
// Save the remaining bytes to the message. | |
asy.dec_message[asy.dec_ptr++] = asy.dec_byte; | |
// The message is completed here (but the CRC must still be verified.) | |
if(asy.dec_ptr == sizeof(asy.dec_message)){ | |
asy.dec_ptr = 0; | |
asy.dec_state = PREFIX; | |
return true; | |
} | |
break; | |
default: | |
break; | |
} | |
// The decoding is iterating. | |
return false; | |
} | |
uint8_t num_enc_symbols() { | |
return 2 * (asy.prefix_seqlen + 1 + sizeof(asy.enc_message)); | |
} | |
protected: | |
uint16_t calc_crc16(volatile uint8_t* d, size_t nbytes) | |
{ | |
// CRC-16 table for the polynomial 0x1021: | |
static PGM_ROM_SPACE uint16_t crc_table[256] = { | |
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 0x8108, | |
0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, 0x1231, 0x0210, | |
0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, 0x9339, 0x8318, 0xb37b, | |
0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, 0x2462, 0x3443, 0x0420, 0x1401, | |
0x64e6, 0x74c7, 0x44a4, 0x5485, 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, | |
0xf5cf, 0xc5ac, 0xd58d, 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, | |
0x5695, 0x46b4, 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, | |
0xc7bc, 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, | |
0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, 0x5af5, | |
0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, 0xdbfd, 0xcbdc, | |
0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, 0x6ca6, 0x7c87, 0x4ce4, | |
0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, 0xedae, 0xfd8f, 0xcdec, 0xddcd, | |
0xad2a, 0xbd0b, 0x8d68, 0x9d49, 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, | |
0x2e32, 0x1e51, 0x0e70, 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, | |
0x9f59, 0x8f78, 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, | |
0xe16f, 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, | |
0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, 0x02b1, | |
0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, 0xb5ea, 0xa5cb, | |
0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, 0x34e2, 0x24c3, 0x14a0, | |
0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xa7db, 0xb7fa, 0x8799, 0x97b8, | |
0xe75f, 0xf77e, 0xc71d, 0xd73c, 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, | |
0x7676, 0x4615, 0x5634, 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, | |
0xb98a, 0xa9ab, 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, | |
0x28a3, 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, | |
0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, 0xfd2e, | |
0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, 0x7c26, 0x6c07, | |
0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, 0xef1f, 0xff3e, 0xcf5d, | |
0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, 0x6e17, 0x7e36, 0x4e55, 0x5e74, | |
0x2e93, 0x3eb2, 0x0ed1, 0x1ef0 | |
}; | |
uint16_t crc; | |
#if defined(QTR_USE_CRC_INITIATOR) && QTR_USE_CRC_INITIATOR == 1 | |
crc = 0xffff; | |
#else | |
crc = 0; | |
#endif | |
for(uint8_t i = 0; i < nbytes; ++i){ | |
uint8_t c = uint8_t(crc >> 8); | |
crc <<= 8; | |
crc ^= pgm_read_word(&crc_table[c ^ d[i]]); | |
} | |
return(crc); | |
} | |
enum state_machine_t{ | |
PREFIX, // looking for the prefix character. | |
ID, // checking the message ID. | |
MESSAGE, // retrieving the message. | |
IDLE, // waiting for the end of encoding. | |
}; | |
volatile struct{ // The "volatile" would be helpful in ISRs. | |
uint8_t prefix; | |
uint8_t id; | |
uint8_t prefix_seqlen; | |
uint8_t prefix_garpat; | |
// For encoding. | |
state_machine_t enc_state; | |
uint8_t enc_message[10]; | |
uint8_t enc_byte; | |
uint8_t enc_ptr; | |
bool enc_byte_complete; | |
// For decoding. | |
state_machine_t dec_state; | |
uint8_t dec_message[10]; | |
uint8_t dec_byte; | |
uint8_t dec_ptr; | |
bool dec_byte_complete; | |
}asy; | |
}; | |
// ------------------------------------------------------------------------------------------------ | |
// Find NMEA-0183 sentences by retrieving the characters in the given "buffer." | |
struct nmea_sentence_t | |
{ | |
struct sentence_t{ | |
std::string command; // NMEA command | |
std::vector<std::string> fields; // NMEA fields | |
int checksum_orig; // The original checksum (if exists) | |
int checksum; // The calculated checksum | |
int nbytes; // The length of the sentence | |
}; | |
// the recovered sentences | |
std::vector<sentence_t> sentences; | |
nmea_sentence_t() | |
{ | |
state = STATE_SOM; | |
tmp_sentence.nbytes = 0; | |
} | |
// Retrieve the characters in the "buffer." | |
bool update(const std::vector<uint8_t>& buffer) | |
{ | |
// Clear the "sentences." | |
if(!sentences.empty()) sentences.clear(); | |
for(auto c : buffer){ | |
c = toupper(c); | |
tmp_sentence.nbytes++; | |
switch(state){ | |
case STATE_SOM: | |
if(c == '$'){ | |
tmp_field.clear(); | |
tmp_sentence.command.clear(); | |
tmp_sentence.fields.clear(); | |
tmp_sentence.checksum = 0; | |
tmp_sentence.nbytes = 1; | |
state = STATE_CMD; | |
} | |
break; | |
case STATE_CMD: | |
if(c != '*'){ | |
// Update the checksum. | |
tmp_sentence.checksum ^= c; | |
// Add this character to the NMEA command. | |
if(c != ','){ | |
if(tmp_sentence.command.length() < MAX_COMMAND_LEN){ | |
tmp_sentence.command.push_back(c); | |
}else{ | |
// The command is too long. | |
state = STATE_SOM; | |
} | |
}else{ | |
if(tmp_sentence.command.length() < MIN_COMMAND_LEN){ | |
// The command is too short. | |
state = STATE_SOM; | |
}else{ | |
state = STATE_FIELD; | |
} | |
} | |
}else{ | |
// The sentence may be unexpectedly terminated. | |
state = STATE_SOM; | |
} | |
break; | |
case STATE_FIELD: | |
if(c != '*'){ | |
if((c!='\r') && (c!='\n')){ | |
// Update the checksum. | |
tmp_sentence.checksum ^= c; | |
// Add this character to the "tmp_field". | |
if(c != ','){ | |
if(tmp_field.length() < MAX_FIELD_LEN){ | |
tmp_field.push_back(c); | |
}else{ | |
// The "tmp_field" is too long to add. | |
state = STATE_SOM; | |
} | |
}else{ | |
if(tmp_sentence.fields.size() >= (MAX_NFIELDS)){ | |
// There are too many fields to be added to this sentence. | |
state = STATE_SOM; | |
}else{ | |
tmp_sentence.fields.push_back(tmp_field); | |
tmp_field.clear(); | |
} | |
} | |
}else{ | |
// This sentence likely lacks a checksum. | |
tmp_sentence.fields.push_back(tmp_field); | |
tmp_sentence.checksum_orig = -1; | |
sentences.push_back(tmp_sentence); | |
state = STATE_SOM; | |
} | |
}else{ | |
tmp_sentence.fields.push_back(tmp_field); | |
state = STATE_CHECKSUM; | |
c_count = 0; | |
} | |
break; | |
case STATE_CHECKSUM: | |
if((c!='\r') && (c!='\n')){ | |
int val = c <= '9' ? c-'0' : c-'A'+10; | |
switch(c_count){ | |
case 0: | |
tmp_sentence.checksum_orig = val<<4; | |
c_count++; | |
break; | |
case 1: | |
tmp_sentence.checksum_orig |= val; | |
state = STATE_EOS; | |
c_count = 0; | |
break; | |
} | |
}else{ | |
tmp_sentence.checksum_orig = -1; | |
state = STATE_EOS; | |
c_count = 0; | |
} | |
break; | |
case STATE_EOS: | |
if((c!='\r') && (c!='\n')){ | |
state = STATE_SOM; | |
}else{ | |
switch(c_count){ | |
case 0: | |
if(c == '\r'){ | |
c_count++; | |
}else{ | |
state = STATE_SOM; | |
} | |
break; | |
case 1: | |
if(c == '\n'){ | |
// Save the sentence. | |
sentences.push_back(tmp_sentence); | |
} | |
state = STATE_SOM; | |
break; | |
} | |
} | |
break; | |
} | |
} | |
return !sentences.empty(); | |
} | |
private: | |
enum{ | |
STATE_SOM, // looking for the leading character '$' | |
STATE_CMD, // retrieving the command (an "NMEA Address") | |
STATE_FIELD, // retrieving fields | |
STATE_CHECKSUM, // retrieving the 8-bit checksum | |
STATE_EOS, // indicating the end of the sentence (<CR><LF>) | |
}state; | |
int c_count; | |
std::string tmp_field; | |
sentence_t tmp_sentence; | |
const size_t MIN_COMMAND_LEN = 5; | |
const size_t MAX_COMMAND_LEN = 5; | |
const size_t MAX_FIELD_LEN = 256; | |
const size_t MAX_NFIELDS = 256; | |
}; | |
// ------------------------------------------------------------------------------------------------ | |
// The serial port type | |
struct serial_port_t | |
{ | |
typedef sp_parity parity_t; | |
typedef sp_transport transport_t; | |
serial_port_t() | |
{ | |
port = nullptr; | |
port_config = nullptr; | |
sp_set_debug_handler(nullptr); | |
} | |
// Open the port with the given name and the parameters (defaulted to 8-n-1.) | |
void open(const std::string& port_name, int32_t baud_rate, | |
uint8_t bits = 8, parity_t parity = SP_PARITY_NONE, uint8_t stopbits = 1) | |
{ | |
if(is_open()) throw std::logic_error(std::string(__func__) + sl_already_open); | |
// Create the port using the "port_name." | |
enum sp_return spr; | |
if((spr = sp_get_port_by_name(port_name.c_str(), &port)) != SP_OK){ | |
throw std::runtime_error(emsg(spr, __func__, "sp_get_port_by_name")); | |
} | |
// Try to open it. | |
if((spr = sp_open(port, SP_MODE_READ_WRITE)) != SP_OK){ | |
throw std::runtime_error(emsg(spr, __func__, "sp_open")); | |
} | |
// Try to set parameters. | |
if(parity == SP_PARITY_INVALID){ | |
throw std::logic_error(std::string(__func__) + "(): is the given parity valid?"); | |
} | |
if((spr = sp_set_baudrate(port, int(baud_rate))) != SP_OK){ | |
throw std::runtime_error(emsg(spr, __func__, "sp_set_baudrate")); | |
} | |
if((spr = sp_set_bits(port, bits)) != SP_OK){ | |
throw std::runtime_error(emsg(spr, __func__, "sp_set_bits")); | |
} | |
if((spr = sp_set_parity(port, parity)) != SP_OK){ | |
throw std::runtime_error(emsg(spr, __func__, "sp_set_parity")); | |
} | |
if((spr = sp_set_stopbits(port, stopbits)) != SP_OK){ | |
throw std::runtime_error(emsg(spr, __func__, "sp_set_stopbits")); | |
} | |
if((spr = sp_set_flowcontrol(port, SP_FLOWCONTROL_NONE)) != SP_OK){ | |
throw std::runtime_error(emsg(spr, __func__, "sp_set_flowcontrol")); | |
} | |
// Save the configuration for further use. | |
if((spr = sp_new_config(&port_config)) != SP_OK){ | |
throw std::runtime_error(emsg(spr, __func__, "sp_new_config")); | |
} | |
if((spr = sp_get_config(port, port_config)) != SP_OK){ | |
throw std::runtime_error(emsg(spr, __func__, "sp_get_config")); | |
} | |
} | |
// Is the port open? | |
bool is_open() | |
{ | |
return port != nullptr; | |
} | |
// Close the port. | |
void close() | |
{ | |
if(is_open()){ | |
// Drain the port as much as possible. | |
enum sp_return spr; | |
if((spr = sp_drain(port)) != SP_OK){ | |
throw std::runtime_error(emsg(spr, __func__, "sp_drain")); | |
} | |
if((spr = sp_close(port)) != SP_OK){ | |
throw std::runtime_error(emsg(spr, __func__, "sp_close")); | |
} | |
sp_free_port(port); | |
sp_free_config(port_config); | |
} | |
} | |
// A thread-safe blocking read that can be invoked independently of writing. | |
size_t read(void* buf, size_t buf_size, uint8_t timeout_ms = 0) | |
{ | |
if(!is_open()) throw std::logic_error(std::string(__func__) + sl_not_open); | |
enum sp_return spr; | |
if((spr = sp_blocking_read_next(port, buf, buf_size, timeout_ms)) < 0){ | |
throw std::runtime_error(emsg(spr, __func__, "sp_blocking_read_next")); | |
} | |
return spr; | |
} | |
// A queued, non-blocking write that can be used to write messages asynchronously. | |
void write(const std::vector<uint8_t>& buf, uint8_t timeout_ms = 0) | |
{ | |
if(!is_open()) throw std::logic_error(std::string(__func__) + sl_not_open); | |
if(!buf.empty()){ | |
tx_queue.enqueue(1, buf, timeout_ms); | |
} | |
} | |
void write(const void* buf, size_t nbytes, uint8_t timeout_ms = 0) | |
{ | |
if(!is_open()) throw std::logic_error(std::string(__func__) + sl_not_open); | |
if(nbytes){ | |
const uint8_t* buf_start = static_cast<const uint8_t *>(buf); | |
tx_queue.enqueue(1, std::vector<uint8_t>(buf_start, buf_start+nbytes), timeout_ms); | |
} | |
} | |
// Get the port name. | |
std::string get_port_name() | |
{ | |
if(!is_open()) throw std::logic_error(std::string(__func__) + sl_not_open); | |
if(char* s = sp_get_port_name(port)){ | |
return s; | |
}else{ | |
throw std::logic_error(std::string(__func__) + "(): a null port name returned!"); | |
} | |
} | |
// Get the port configuration. | |
void get_basic_config(int& baudrate, int& bits, parity_t& parity, int& stopbits) | |
{ | |
if(!is_open()) throw std::logic_error(std::string(__func__) + sl_not_open); | |
enum sp_return spr; | |
if((spr = sp_get_config_baudrate(port_config, &baudrate)) != SP_OK){ | |
throw std::runtime_error(emsg(spr, __func__, "sp_get_config_baudrate")); | |
} | |
if((spr = sp_get_config_bits(port_config, &bits)) != SP_OK){ | |
throw std::runtime_error(emsg(spr, __func__, "sp_get_config_bits")); | |
} | |
if((spr = sp_get_config_parity(port_config, &parity)) != SP_OK){ | |
throw std::runtime_error(emsg(spr, __func__, "sp_get_config_parity")); | |
} | |
if((spr = sp_get_config_stopbits(port_config, &stopbits)) != SP_OK){ | |
throw std::runtime_error(emsg(spr, __func__, "sp_get_config_stopbits")); | |
} | |
} | |
// Get the port description. | |
std::string get_port_description() | |
{ | |
if(!is_open()) throw std::logic_error(std::string(__func__) + sl_not_open); | |
if(char* s = sp_get_port_description(port)){ | |
return s; | |
} | |
return ""; | |
} | |
// Query the transport of the port goes connected through. | |
transport_t get_port_transport() | |
{ | |
if(!is_open()) throw std::logic_error(std::string(__func__) + sl_not_open); | |
return sp_get_port_transport(port); | |
} | |
// Get the USB address of the port. | |
bool get_usb_bus_address(int& bus, int& address) | |
{ | |
if(!is_open()) throw std::logic_error(std::string(__func__) + sl_not_open); | |
if(sp_get_port_usb_bus_address(port, &bus, &address) == SP_OK){ | |
return true; | |
} | |
return false; | |
} | |
// Get "vid" and "pid" of the port. | |
bool get_usb_vid_pid(int& vid, int& pid) | |
{ | |
if(!is_open()) throw std::logic_error(std::string(__func__) + sl_not_open); | |
if(sp_get_port_usb_vid_pid(port, &vid, &pid) == SP_OK){ | |
return true; | |
} | |
return false; | |
} | |
// Get the product name. | |
std::string get_usb_product_name() | |
{ | |
if(!is_open()) throw std::logic_error(std::string(__func__) + sl_not_open); | |
if(char* s = sp_get_port_usb_product(port)){ | |
return s; | |
} | |
return ""; | |
} | |
// Get the serial number. | |
std::string get_usb_serial_number() | |
{ | |
if(!is_open()) throw std::logic_error(std::string(__func__) + sl_not_open); | |
if(char* s = sp_get_port_usb_serial(port)){ | |
return s; | |
} | |
return ""; | |
} | |
// Get the manufacturer name. | |
std::string get_usb_manufacturer() | |
{ | |
if(!is_open()) throw std::logic_error(std::string(__func__) + sl_not_open); | |
if(char* s = sp_get_port_usb_manufacturer(port)){ | |
return s; | |
} | |
return ""; | |
} | |
// Get the Bluetooth address (in text format.) | |
std::string get_bluetooth_address() | |
{ | |
if(!is_open()) throw std::logic_error(std::string(__func__) + sl_not_open); | |
if(char* s = sp_get_port_bluetooth_address(port)){ | |
return s; | |
} | |
return ""; | |
} | |
// Get the parity type (as string.) | |
static std::string get_parity_string(parity_t parity) | |
{ | |
switch(parity){ | |
case SP_PARITY_EVEN: | |
return "even"; | |
case SP_PARITY_ODD: | |
return "odd"; | |
case SP_PARITY_NONE: | |
return "none"; | |
case SP_PARITY_MARK: | |
return "mark"; | |
case SP_PARITY_SPACE: | |
return "space"; | |
default: | |
return "invalid"; | |
} | |
} | |
// Get the frame duration (with the current configuration.) | |
double frame_duration() | |
{ | |
if(!is_open()) throw std::logic_error(std::string(__func__) + sl_not_open); | |
int baudrate, bits, stopbits; | |
parity_t parity; | |
get_basic_config(baudrate, bits, parity, stopbits); | |
if(parity == SP_PARITY_INVALID){ | |
throw std::logic_error(std::string(__func__) + "(): invalid parity type!"); | |
} | |
double nbits = 1 + bits + stopbits + ((parity==SP_PARITY_NONE) ? 0 : 1); | |
return nbits / baudrate; | |
} | |
~serial_port_t() | |
{ | |
// Stop the TX backend. | |
tx_queue.enqueue(0, std::vector<uint8_t>{}, 0); | |
if(tx_thread.joinable()) tx_thread.join(); | |
if(is_open()){ | |
sp_drain(port); | |
sp_close(port); | |
sp_free_port(port); | |
sp_free_config(port_config); | |
} | |
} | |
protected: | |
struct sp_port* port; | |
struct sp_port_config* port_config; | |
const char* sl_already_open = "(): this port is already open."; | |
const char* sl_not_open = "(): this port is not yet open."; | |
// The TX message queue. | |
eventpp::EventQueue<int, void(const std::vector<uint8_t>&, uint8_t)> tx_queue; | |
// The TX backend. | |
std::thread tx_thread = std::thread([&]() | |
{ | |
bool exit_pending = false; | |
tx_queue.appendListener(0, [&exit_pending](const std::vector<uint8_t>&, uint8_t){ | |
exit_pending = true; | |
}); | |
tx_queue.appendListener(1, [&](const std::vector<uint8_t>& buf, uint8_t timeout){ | |
enum sp_return spr = sp_blocking_write(port, &buf[0], buf.size(), timeout); | |
if(spr < 0){ | |
if(get_port_name().find("tnt") != std::string::npos){ | |
std::cerr << "Unable to write to the port.\n" | |
"Check that the opposite process is enabled when using 'tty0tty'."; | |
} | |
throw std::runtime_error(emsg(spr, "thread_tx", "sp_blocking_write")); | |
} | |
if(size_t(spr) < buf.size()){ | |
if(get_port_description().find("com0com") != std::string::npos){ | |
std::cerr << "Unable to write to the port.\n" | |
"Check that the opposite process is enabled when using 'com0com'."; | |
} | |
throw std::runtime_error("thread_tx(): sp_blocking_write(): TX timed out."); | |
} | |
}); | |
while(!exit_pending){ | |
tx_queue.wait(); | |
tx_queue.process(); | |
} | |
}); | |
// Merge error messages. | |
std::string emsg(enum sp_return spr, const char* where, const char* func_name) | |
{ | |
const char* s; | |
char* sp_msg = sp_last_error_message(); | |
if(sp_msg){ | |
s = sp_msg; | |
}else{ | |
s = "sp_last_error_message() returns null."; | |
} | |
char buf[1024]; | |
snprintf(buf, sizeof(buf)-1, "%s(): %s(): Error %d: %s", where, func_name, spr, s); | |
if(sp_msg){ | |
sp_free_error_message(sp_msg); | |
} | |
return buf; | |
} | |
}; | |
// ------------------------------------------------------------------------------------------------ | |
/* The timer type | |
* This timer emits the system time to the callbacks at the specified event. The upper limit of the | |
* time expressed is 07:12:55 August 17th, 292278994. | |
*/ | |
struct tc | |
{ | |
struct tc_time_t { | |
struct TM local_time; | |
struct TM utc; | |
uint16_t ms; | |
}; | |
eventpp::CallbackList<void(const tc_time_t&, bool)> callbacks; | |
/* The callbacks can be registered like this: | |
* tc timer; | |
* timer.callback.append([&](const tc::tc_time_t& time, bool timing_ok){ | |
* if(timing_ok){ | |
* perform_operation_A_with(time); | |
* }else{ | |
* // This event may take longer than expected due to the system overhead, | |
* // causing a significant delay compared to the emitted time. | |
* std::cerr << "A timing glitch might have occurred.\n"; | |
* } | |
* }); | |
* timer.callback.append([&](const tc::tc_time_t& time, bool){ | |
* perform_operation_B_with(time); | |
* }); | |
* timer.start(); | |
*/ | |
// Start the timer (even if the CallbackList is empty.) | |
void start(uint32_t interval_ms = 100, bool single_shot = false) | |
{ | |
if(timer_started) return; | |
timer_started = true; | |
timer_thread = std::thread([interval_ms, single_shot, this]{ | |
using namespace std::chrono; | |
tc_time_t tc_time; | |
bool timing_ok = true; | |
std::unique_lock <std::mutex> lock(mtx); | |
int worst_precision_ms = round(interval_ms * worst_precision/100.0); | |
int timing_err_ms = 0; | |
do{ | |
read_system_time(tc_time, interval_ms); | |
auto callback_starts = mono_clock.now(); | |
callbacks(tc_time, timing_ok); | |
int until = interval_ms - worst_precision_ms; | |
condvar.wait_until(lock, callback_starts + milliseconds(until)); | |
duration <double> z = mono_clock.now() - callback_starts; | |
int err_z = floor(z.count()*1000 - until); | |
timing_err_ms = err_z > timing_err_ms ? err_z : timing_err_ms; | |
if((z.count()*1000) >= (interval_ms-timing_err_ms-min_overlap_ms)){ | |
if(worst_precision_ms < (int(interval_ms)/2)){ | |
worst_precision_ms += timing_err_ms/2; | |
} | |
timing_ok = false; | |
}else{ | |
timing_ok = true; | |
} | |
}while(timer_started & (!single_shot)); | |
}); | |
} | |
// Stop the timer. (The CallbackList is retained for the timer to be restarted.) | |
void stop() | |
{ | |
if(!timer_started) return; | |
timer_started = false; | |
condvar.notify_all(); | |
if(timer_thread.joinable()) timer_thread.join(); | |
} | |
// Clear the callback list. | |
void clear_callbacks() | |
{ | |
using CL = eventpp::CallbackList<void(const tc_time_t&, bool)>; | |
callbacks.forEach([&](const CL::Handle& handle, const CL::Callback& /*callback*/){ | |
callbacks.remove(handle); | |
}); | |
} | |
// Set an offset to compensate for the time reading (in ms.) | |
void set_time_offset(int64_t offset_ms) | |
{ | |
offset = offset_ms; | |
} | |
tc() | |
{ | |
timer_started = false; | |
} | |
~tc() | |
{ | |
stop(); | |
} | |
protected: | |
int64_t offset; | |
bool timer_started; | |
std::chrono::system_clock wall_clock; | |
std::chrono::steady_clock mono_clock; | |
std::mutex mtx; | |
std::condition_variable condvar; | |
std::thread timer_thread; | |
const double worst_precision = 5; | |
const int min_overlap_ms = 5; | |
const int next_attempt_us = 500; | |
void read_system_time(tc_time_t& tc_time, uint32_t interval_ms) | |
{ | |
using namespace std::chrono; | |
auto read_wall_clock = [this] (){ | |
auto now = wall_clock.now(); | |
int64_t ticks = duration_cast<milliseconds>(now.time_since_epoch()).count(); | |
return ticks + offset; | |
}; | |
auto floor_to = [](uint32_t interval, int64_t ticks){ | |
return int64_t(ticks / interval) * interval; | |
}; | |
int64_t tims, tmp_tims = floor_to(interval_ms, read_wall_clock()); | |
while(1){ | |
int64_t ticks_ms = read_wall_clock(); | |
tims = floor_to(interval_ms, ticks_ms); | |
if(tims > tmp_tims){ | |
Time64_T ticks_sec = ticks_ms / 1000; | |
localtime64_r(&ticks_sec, &tc_time.local_time); | |
gmtime64_r(&ticks_sec, &tc_time.utc); | |
tc_time.ms = ticks_ms % 1000; | |
break; | |
}else{ | |
std::this_thread::sleep_for(microseconds(next_attempt_us)); | |
} | |
} | |
} | |
}; | |
// ------------------------------------------------------------------------------------------------ | |
namespace help_messages | |
{ | |
#ifndef CVSVER | |
#define CVSVER "v1.0-defaulted" | |
#endif | |
inline std::string version_info() | |
{ | |
return fmt::format("time2tty, {} Copyright (C) 2010-2023 BD1ES\nThis program is " | |
"used to test the serial communication of the QTR Clock AVR.\n", | |
CVSVER); | |
} | |
inline std::string libraries_used() | |
{ | |
return R"( | |
"time2tty" takes advantage of the following open-source software: | |
libserialport, https://sigrok.org/wiki/Libserialport | |
y2038, https://github.com/evalEmpire/y2038 | |
eventpp, https://github.com/wqking/eventpp | |
rang, https://github.com/agauniyal/rang | |
fmt, https://github.com/fmtlib/fmt | |
)"; | |
} | |
inline std::string usage() | |
{ | |
return R"( | |
Usage: time2tty [options] <port name> <Baud> [data bits] [parity] [stop bits] | |
Examples: time2tty -a1 -t -otr /dev/ttyS1 9600 | |
time2tty -q2,59 -ort /dev/ttyAMA0 2400 8 n 1 | |
Options: | |
-o --operation=<[t][r][tr|rt]> | |
Enable the TX and RX lines for the serial port. (defaulted to -or.) | |
t: enable TX; r: enable RX; tr or rt: enable full duplex mode. | |
-q --QTR=[[m],[n],[p]] (Period "p" must be in the 1-3600 range.) | |
Enable QTR (type 1) messages. (defaulted to -q60,0,60.) | |
m,n,p: send messages m times from the nth second over p seconds. | |
-t --TOD=[prefix] | |
Enable TOD/NMEA-0183 "ZDA" messages. (defaulted to -tGP for GPZDA.) | |
prefix: any combination of two letters, such as GP or GN. | |
-a --advance=<n> | |
Send the message n ms ahead of the current time. (-n lags) | |
-x --hex | |
Print the received messages as hexadecimal numbers. | |
Arguments: | |
port name: System serial ports like COM3, /dev/ttyS1, and /dev/ttyUSB0. | |
Baud: The rate at which the signal state changes, such as 9600, 115200. | |
data bits: Bits in each data frame, ranging from 5-8. (defaulted to 8.) | |
parity: Parity types E (even,) O (odd,) and N (none.) (defaulted to n.) | |
stop bits: Idle bits (1 or 2) that chain data frames. (defaulted to 1.) | |
)"; | |
} | |
inline std::string comments() | |
{ | |
return R"( | |
Additional Information | |
TXT message: | |
The TXT message is a line of text that appears like "2020-07-15 10:35:26.000." | |
It uses the RFC-3339 format to print the clock time with millisecond accuracy. | |
This message is used to check if MCU's UART routines are functioning. | |
While operating, the option "-ot" allows this command to transmit TXT messages | |
every second, and the option "-or" makes the RX print the received messages to | |
the console. Note that a Vim-like hex printing can be inserted into the output | |
if the flag "-x" is added to the command. | |
(Time2tty adds a fractional number to the printout on Linux platforms, showing | |
the system clock's frequency drifts in ppm.) | |
QTR message: | |
The QTR message is a fixed-length Hamming sequence programmed to transmit time | |
to slave clocks. However, time2tty only supports the type 1 message. The other | |
involves Manchester coding and is for MCU's firmware rather than PC. | |
If the option "-ot" is enabled, time2tty emits the type 1 message to the slave | |
clocks each second (or at the specified interval.) Meanwhile, the option "-or" | |
enables time2tty to listen to the sender and print the decoded messages. While | |
"-x" is activated, the hex dump appears before the recovered information. | |
TOD message: | |
The TOD message resembles the NMEA $GPZDA sentence. GPS receivers emit similar | |
messages so that users can retrieve the satellite time. GPS-disciplined clocks | |
also provide $GPZDA options for timekeeping systems to regulate their times by | |
locking the PPS occurrences. In this case, "$GPZDA" is called "TOD" by some of | |
the device vendors. | |
With "-ot," time2tty sends the listener TOD messages once every second, and it | |
can listen to the sender when the "-or" option is enabled. The "-x" allows the | |
user to examine the decoded sentence with hex dumps. | |
Comments: | |
While time2tty is designed for serial debugging, it can be used as a backup of | |
the master clock running QTR messages. This redundancy aids in maintaining the | |
timing accuracy of 10 ms or similar for the slave clocks if the hardware-based | |
master clock is undergoing maintenance. | |
However, stabilizing the clock reading is essential when time2tty is operating | |
as a time source, sometimes meaning synchronizing the system clock to the GNSS | |
or nternet time references (such as NTP or Chrony.) | |
)"; | |
} | |
} | |
#endif // TIME2TTY_H |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment