Skip to content

Instantly share code, notes, and snippets.

@bd1es
Last active June 23, 2024 12:19
Show Gist options
  • Save bd1es/262569cd516ed16019e4ced12fa82327 to your computer and use it in GitHub Desktop.
Save bd1es/262569cd516ed16019e4ced12fa82327 to your computer and use it in GitHub Desktop.
time2tty, for a NanoPi NEO2 running Armbian
/* 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();
}
/* 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