/* trackuino copyright (C) 2010 EA5HAV Javi * * 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 2 * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "config.h" #include "aprs.h" #include #include #include #include "debug.h" #include "base91.h" #include "digipeater.h" #include "dedupe.h" #include "radio.h" #include "flash.h" #include "image.h" #define METER_TO_FEET(m) (((m)*26876) / 8192) typedef struct { sysinterval_t time; char call[AX25_MAX_ADDR_LEN]; } heard_t; static uint16_t msg_id; char alias_re[] = "WIDE[4-7]-[1-7]|CITYD"; char wide_re[] = "WIDE[1-7]-[1-7]"; enum preempt_e preempt = PREEMPT_OFF; static heard_t heard_list[APRS_HEARD_LIST_SIZE]; static bool dedupe_initialized; const conf_command_t command_list[] = { {TYPE_INT, "pos_pri.active", sizeof(conf_sram.pos_pri.beacon.active), &conf_sram.pos_pri.beacon.active }, {TYPE_TIME, "pos_pri.init_delay", sizeof(conf_sram.pos_pri.beacon.init_delay), &conf_sram.pos_pri.beacon.init_delay }, {TYPE_INT, "pos_pri.sleep_conf.type", sizeof(conf_sram.pos_pri.beacon.sleep_conf.type), &conf_sram.pos_pri.beacon.sleep_conf.type }, {TYPE_INT, "pos_pri.sleep_conf.vbat_thres", sizeof(conf_sram.pos_pri.beacon.sleep_conf.vbat_thres), &conf_sram.pos_pri.beacon.sleep_conf.vbat_thres }, {TYPE_INT, "pos_pri.sleep_conf.vsol_thres", sizeof(conf_sram.pos_pri.beacon.sleep_conf.vsol_thres), &conf_sram.pos_pri.beacon.sleep_conf.vsol_thres }, {TYPE_TIME, "pos_pri.cycle", sizeof(conf_sram.pos_pri.beacon.cycle), &conf_sram.pos_pri.beacon.cycle }, {TYPE_INT, "pos_pri.pwr", sizeof(conf_sram.pos_pri.radio_conf.pwr), &conf_sram.pos_pri.radio_conf.pwr }, {TYPE_INT, "pos_pri.freq", sizeof(conf_sram.pos_pri.radio_conf.freq), &conf_sram.pos_pri.radio_conf.freq }, {TYPE_INT, "pos_pri.mod", sizeof(conf_sram.pos_pri.radio_conf.mod), &conf_sram.pos_pri.radio_conf.mod }, {TYPE_INT, "pos_pri.cca", sizeof(conf_sram.pos_pri.radio_conf.cca), &conf_sram.pos_pri.radio_conf.cca }, {TYPE_STR, "pos_pri.call", sizeof(conf_sram.pos_pri.call), &conf_sram.pos_pri.call }, {TYPE_STR, "pos_pri.path", sizeof(conf_sram.pos_pri.path), &conf_sram.pos_pri.path }, {TYPE_INT, "pos_pri.symbol", sizeof(conf_sram.pos_pri.symbol), &conf_sram.pos_pri.symbol }, {TYPE_INT, "pos_pri.aprs_msg", sizeof(conf_sram.pos_pri.aprs_msg), &conf_sram.pos_pri.aprs_msg }, {TYPE_INT, "pos_sec.active", sizeof(conf_sram.pos_sec.beacon.active), &conf_sram.pos_sec.beacon.active }, {TYPE_TIME, "pos_sec.init_delay", sizeof(conf_sram.pos_sec.beacon.init_delay), &conf_sram.pos_sec.beacon.init_delay }, {TYPE_INT, "pos_sec.sleep_conf.type", sizeof(conf_sram.pos_sec.beacon.sleep_conf.type), &conf_sram.pos_sec.beacon.sleep_conf.type }, {TYPE_INT, "pos_sec.sleep_conf.vbat_thres", sizeof(conf_sram.pos_sec.beacon.sleep_conf.vbat_thres), &conf_sram.pos_sec.beacon.sleep_conf.vbat_thres }, {TYPE_INT, "pos_sec.sleep_conf.vsol_thres", sizeof(conf_sram.pos_sec.beacon.sleep_conf.vsol_thres), &conf_sram.pos_sec.beacon.sleep_conf.vsol_thres }, {TYPE_TIME, "pos_sec.cycle", sizeof(conf_sram.pos_sec.beacon.cycle), &conf_sram.pos_sec.beacon.cycle }, {TYPE_INT, "pos_sec.pwr", sizeof(conf_sram.pos_sec.radio_conf.pwr), &conf_sram.pos_sec.radio_conf.pwr }, {TYPE_INT, "pos_sec.freq", sizeof(conf_sram.pos_sec.radio_conf.freq), &conf_sram.pos_sec.radio_conf.freq }, {TYPE_INT, "pos_sec.mod", sizeof(conf_sram.pos_sec.radio_conf.mod), &conf_sram.pos_sec.radio_conf.mod }, {TYPE_INT, "pos_sec.cca", sizeof(conf_sram.pos_sec.radio_conf.cca), &conf_sram.pos_sec.radio_conf.cca }, {TYPE_STR, "pos_sec.call", sizeof(conf_sram.pos_sec.call), &conf_sram.pos_sec.call }, {TYPE_STR, "pos_sec.path", sizeof(conf_sram.pos_sec.path), &conf_sram.pos_sec.path }, {TYPE_INT, "pos_sec.symbol", sizeof(conf_sram.pos_sec.symbol), &conf_sram.pos_sec.symbol }, {TYPE_INT, "pos_sec.aprs_msg", sizeof(conf_sram.pos_sec.aprs_msg), &conf_sram.pos_sec.aprs_msg }, {TYPE_INT, "img_pri.active", sizeof(conf_sram.img_pri.svc_conf.active), &conf_sram.img_pri.svc_conf.active }, {TYPE_TIME, "img_pri.init_delay", sizeof(conf_sram.img_pri.svc_conf.init_delay), &conf_sram.img_pri.svc_conf.init_delay }, {TYPE_TIME, "img_pri.send_spacing", sizeof(conf_sram.img_pri.svc_conf.send_spacing), &conf_sram.img_pri.svc_conf.send_spacing }, {TYPE_INT, "img_pri.sleep_conf.type", sizeof(conf_sram.img_pri.svc_conf.sleep_conf.type), &conf_sram.img_pri.svc_conf.sleep_conf.type }, {TYPE_INT, "img_pri.sleep_conf.vbat_thres", sizeof(conf_sram.img_pri.svc_conf.sleep_conf.vbat_thres), &conf_sram.img_pri.svc_conf.sleep_conf.vbat_thres }, {TYPE_INT, "img_pri.sleep_conf.vsol_thres", sizeof(conf_sram.img_pri.svc_conf.sleep_conf.vsol_thres), &conf_sram.img_pri.svc_conf.sleep_conf.vsol_thres }, {TYPE_TIME, "img_pri.cycle", sizeof(conf_sram.img_pri.svc_conf.cycle), &conf_sram.img_pri.svc_conf.cycle }, {TYPE_INT, "img_pri.pwr", sizeof(conf_sram.img_pri.radio_conf.pwr), &conf_sram.img_pri.radio_conf.pwr }, {TYPE_INT, "img_pri.freq", sizeof(conf_sram.img_pri.radio_conf.freq), &conf_sram.img_pri.radio_conf.freq }, {TYPE_INT, "img_pri.mod", sizeof(conf_sram.img_pri.radio_conf.mod), &conf_sram.img_pri.radio_conf.mod }, {TYPE_INT, "img_pri.cca", sizeof(conf_sram.img_pri.radio_conf.cca), &conf_sram.img_pri.radio_conf.cca }, {TYPE_INT, "img_pri.speed", sizeof(conf_sram.img_pri.radio_conf.speed), &conf_sram.img_pri.radio_conf.speed }, {TYPE_INT, "img_pri.redundantTx", sizeof(conf_sram.img_pri.redundantTx), &conf_sram.img_pri.redundantTx }, {TYPE_STR, "img_pri.call", sizeof(conf_sram.img_pri.call), &conf_sram.img_pri.call }, {TYPE_STR, "img_pri.path", sizeof(conf_sram.img_pri.path), &conf_sram.img_pri.path }, {TYPE_INT, "img_pri.res", sizeof(conf_sram.img_pri.res), &conf_sram.img_pri.res }, {TYPE_INT, "img_pri.quality", sizeof(conf_sram.img_pri.quality), &conf_sram.img_pri.quality }, {TYPE_INT, "img_pri.buf_size", sizeof(conf_sram.img_pri.buf_size), &conf_sram.img_pri.buf_size }, {TYPE_INT, "img_sec.active", sizeof(conf_sram.img_sec.svc_conf.active), &conf_sram.img_sec.svc_conf.active }, {TYPE_TIME, "img_sec.init_delay", sizeof(conf_sram.img_sec.svc_conf.init_delay), &conf_sram.img_sec.svc_conf.init_delay }, {TYPE_TIME, "img_sec.send_spacing", sizeof(conf_sram.img_sec.svc_conf.send_spacing), &conf_sram.img_sec.svc_conf.send_spacing }, {TYPE_INT, "img_sec.sleep_conf.type", sizeof(conf_sram.img_sec.svc_conf.sleep_conf.type), &conf_sram.img_sec.svc_conf.sleep_conf.type }, {TYPE_INT, "img_sec.sleep_conf.vbat_thres", sizeof(conf_sram.img_sec.svc_conf.sleep_conf.vbat_thres), &conf_sram.img_sec.svc_conf.sleep_conf.vbat_thres }, {TYPE_INT, "img_sec.sleep_conf.vsol_thres", sizeof(conf_sram.img_sec.svc_conf.sleep_conf.vsol_thres), &conf_sram.img_sec.svc_conf.sleep_conf.vsol_thres }, {TYPE_TIME, "img_sec.cycle", sizeof(conf_sram.img_sec.svc_conf.cycle), &conf_sram.img_sec.svc_conf.cycle }, {TYPE_INT, "img_sec.pwr", sizeof(conf_sram.img_sec.radio_conf.pwr), &conf_sram.img_sec.radio_conf.pwr }, {TYPE_INT, "img_sec.freq", sizeof(conf_sram.img_sec.radio_conf.freq), &conf_sram.img_sec.radio_conf.freq }, {TYPE_INT, "img_sec.mod", sizeof(conf_sram.img_sec.radio_conf.mod), &conf_sram.img_sec.radio_conf.mod }, {TYPE_INT, "img_sec.cca", sizeof(conf_sram.img_sec.radio_conf.cca), &conf_sram.img_sec.radio_conf.cca }, {TYPE_INT, "img_sec.speed", sizeof(conf_sram.img_sec.radio_conf.speed), &conf_sram.img_sec.radio_conf.speed }, {TYPE_INT, "img_sec.redundantTx", sizeof(conf_sram.img_sec.redundantTx), &conf_sram.img_sec.redundantTx }, {TYPE_STR, "img_sec.call", sizeof(conf_sram.img_sec.call), &conf_sram.img_sec.call }, {TYPE_STR, "img_sec.path", sizeof(conf_sram.img_sec.path), &conf_sram.img_sec.path }, {TYPE_INT, "img_sec.res", sizeof(conf_sram.img_sec.res), &conf_sram.img_sec.res }, {TYPE_INT, "img_sec.quality", sizeof(conf_sram.img_sec.quality), &conf_sram.img_sec.quality }, {TYPE_INT, "img_sec.buf_size", sizeof(conf_sram.img_sec.buf_size), &conf_sram.img_sec.buf_size }, {TYPE_INT, "log.active", sizeof(conf_sram.log.svc_conf.active), &conf_sram.log.svc_conf.active }, {TYPE_TIME, "log.init_delay", sizeof(conf_sram.log.svc_conf.init_delay), &conf_sram.log.svc_conf.init_delay }, {TYPE_TIME, "log.send_spacing", sizeof(conf_sram.log.svc_conf.send_spacing), &conf_sram.log.svc_conf.send_spacing }, {TYPE_INT, "log.sleep_conf.type", sizeof(conf_sram.log.svc_conf.sleep_conf.type), &conf_sram.log.svc_conf.sleep_conf.type }, {TYPE_INT, "log.sleep_conf.vbat_thres", sizeof(conf_sram.log.svc_conf.sleep_conf.vbat_thres), &conf_sram.log.svc_conf.sleep_conf.vbat_thres }, {TYPE_INT, "log.sleep_conf.vsol_thres", sizeof(conf_sram.log.svc_conf.sleep_conf.vsol_thres), &conf_sram.log.svc_conf.sleep_conf.vsol_thres }, {TYPE_TIME, "log.cycle", sizeof(conf_sram.log.svc_conf.cycle), &conf_sram.log.svc_conf.cycle }, {TYPE_INT, "log.pwr", sizeof(conf_sram.log.radio_conf.pwr), &conf_sram.log.radio_conf.pwr }, {TYPE_INT, "log.freq", sizeof(conf_sram.log.radio_conf.freq), &conf_sram.log.radio_conf.freq }, {TYPE_INT, "log.mod", sizeof(conf_sram.log.radio_conf.mod), &conf_sram.log.radio_conf.mod }, {TYPE_INT, "log.cca", sizeof(conf_sram.log.radio_conf.cca), &conf_sram.log.radio_conf.cca }, {TYPE_INT, "log.speed", sizeof(conf_sram.log.radio_conf.speed), &conf_sram.log.radio_conf.speed }, {TYPE_STR, "log.call", sizeof(conf_sram.log.call), &conf_sram.log.call }, {TYPE_STR, "log.path", sizeof(conf_sram.log.path), &conf_sram.log.path }, {TYPE_INT, "log.density", sizeof(conf_sram.log.density), &conf_sram.log.density }, {TYPE_INT, "aprs.rx.active", sizeof(conf_sram.aprs.rx.svc_conf.active), &conf_sram.aprs.rx.svc_conf.active }, {TYPE_TIME, "aprs.rx.init_delay", sizeof(conf_sram.aprs.rx.svc_conf.init_delay), &conf_sram.aprs.rx.svc_conf.init_delay }, {TYPE_INT, "aprs.rx.freq", sizeof(conf_sram.aprs.rx.radio_conf.freq), &conf_sram.aprs.rx.radio_conf.freq }, {TYPE_INT, "aprs.rx.mod", sizeof(conf_sram.aprs.rx.radio_conf.mod), &conf_sram.aprs.rx.radio_conf.mod }, {TYPE_INT, "aprs.rx.speed", sizeof(conf_sram.aprs.rx.radio_conf.speed), &conf_sram.aprs.rx.radio_conf.speed }, {TYPE_STR, "aprs.rx.call", sizeof(conf_sram.aprs.rx.call), &conf_sram.aprs.rx.call }, {TYPE_INT, "aprs.digi", sizeof(conf_sram.aprs.digi), &conf_sram.aprs.digi }, {TYPE_INT, "aprs.tx.freq", sizeof(conf_sram.aprs.tx.radio_conf.freq), &conf_sram.aprs.tx.radio_conf.freq }, {TYPE_INT, "aprs.tx.pwr", sizeof(conf_sram.aprs.tx.radio_conf.pwr), &conf_sram.aprs.tx.radio_conf.pwr }, {TYPE_INT, "aprs.tx.mod", sizeof(conf_sram.aprs.tx.radio_conf.mod), &conf_sram.aprs.tx.radio_conf.mod }, {TYPE_INT, "aprs.tx.cca", sizeof(conf_sram.aprs.tx.radio_conf.cca), &conf_sram.aprs.tx.radio_conf.cca }, {TYPE_STR, "aprs.tx.call", sizeof(conf_sram.aprs.tx.call), &conf_sram.aprs.tx.call }, {TYPE_STR, "aprs.tx.path", sizeof(conf_sram.aprs.tx.path), &conf_sram.aprs.tx.path }, {TYPE_INT, "aprs.tx.symbol", sizeof(conf_sram.aprs.tx.symbol), &conf_sram.aprs.tx.symbol }, {TYPE_INT, "aprs.beacon", sizeof(conf_sram.aprs.tx.beacon.active), &conf_sram.aprs.tx.beacon.active }, {TYPE_INT, "aprs.beacon.lat", sizeof(conf_sram.aprs.tx.beacon.lat), &conf_sram.aprs.tx.beacon.lat }, {TYPE_INT, "aprs.beacon.lon", sizeof(conf_sram.aprs.tx.beacon.lon), &conf_sram.aprs.tx.beacon.lon }, {TYPE_INT, "aprs.beacon.alt", sizeof(conf_sram.aprs.tx.beacon.alt), &conf_sram.aprs.tx.beacon.alt }, {TYPE_INT, "aprs.beacon.cycle", sizeof(conf_sram.aprs.tx.beacon.cycle), &conf_sram.aprs.tx.beacon.cycle }, {TYPE_INT, "keep_cam_switched_on", sizeof(conf_sram.keep_cam_switched_on), &conf_sram.keep_cam_switched_on }, {TYPE_INT, "gps_on_vbat", sizeof(conf_sram.gps_on_vbat), &conf_sram.gps_on_vbat }, {TYPE_INT, "gps_off_vbat", sizeof(conf_sram.gps_off_vbat), &conf_sram.gps_off_vbat }, {TYPE_INT, "gps_onper_vbat", sizeof(conf_sram.gps_onper_vbat), &conf_sram.gps_onper_vbat }, {TYPE_INT, "gps_pressure", sizeof(conf_sram.gps_pressure), &conf_sram.gps_pressure }, {TYPE_INT, "gps_low_alt", sizeof(conf_sram.gps_low_alt), &conf_sram.gps_low_alt }, {TYPE_INT, "gps_high_alt", sizeof(conf_sram.gps_high_alt), &conf_sram.gps_high_alt }, {TYPE_INT, "default.freq", sizeof(conf_sram.freq), &conf_sram.freq }, {TYPE_INT, "base.freq", sizeof(conf_sram.base.radio_conf.freq), &conf_sram.base.radio_conf.freq }, {TYPE_INT, "base.pwr", sizeof(conf_sram.base.radio_conf.pwr), &conf_sram.base.radio_conf.pwr }, {TYPE_INT, "base.mod", sizeof(conf_sram.base.radio_conf.mod), &conf_sram.base.radio_conf.mod }, {TYPE_INT, "base.cca", sizeof(conf_sram.base.radio_conf.cca), &conf_sram.base.radio_conf.cca }, {TYPE_STR, "base.call", sizeof(conf_sram.base.call), &conf_sram.base.call }, {TYPE_TIME, "tel_enc_cycle", sizeof(conf_sram.tel_enc_cycle), &conf_sram.tel_enc_cycle }, {TYPE_NULL} }; /* * Table of commands that can be embedded in a message. */ const APRSCommand aprs_commands[] = { {"?aprsd", aprs_send_aprsd_message}, {"?aprsh", aprs_send_aprsh_message}, {"?aprsp", aprs_send_position_response}, {"?gpio", aprs_execute_gpio_command}, {"?reset", aprs_execute_system_reset}, {"?save", aprs_execute_config_save}, {"?img", aprs_execute_img_command}, {"?config", aprs_execute_config_command}, {NULL, NULL} }; /** * @brief parse arguments from a command string. * * @return pointer to next element in string. * @retval NULL if end. */ static char *aprs_parse_arguments(char *str, char **saveptr) { char *p; if (str != NULL) *saveptr = str; p = *saveptr; if (!p) { return NULL; } /* Skipping white space.*/ p += strspn(p, " \t"); if (*p == '"') { /* If an argument starts with a double quote then its delimiter is another quote.*/ p++; *saveptr = strpbrk(p, "\""); } else { /* The delimiter is white space.*/ *saveptr = strpbrk(p, " \t"); } /* Replacing the delimiter with a zero.*/ if (*saveptr != NULL) { *(*saveptr)++ = '\0'; } return *p != '\0' ? p : NULL; } /** * @brief Execute a command in an APRS message. * @notes Known commands are in APRS command table. * @notes Commands themselves return only MSG_OK or MSG_ERROR. * * @return result of command. * @retval MSG_OK if the command completed. * @retval MSG_ERROR if there was an error in command execution. * @retval MSG_TIMEOUT if the command was not found in known commands. */ static msg_t aprs_cmd_exec(const APRSCommand *acp, char *name, aprs_identity_t *id, int argc, char *argv[]) { while (acp->ac_name != NULL) { if (strcmp(acp->ac_name, name) == 0) { return acp->ac_function(id, argc, argv); } acp++; } return MSG_TIMEOUT; } /** * */ void aprs_get_identity(void) { } /** * */ void aprs_debug_getPacket(packet_t pp, char* buf, uint32_t len) { // Decode packet char rec[127]; unsigned char *pinfo; ax25_format_addrs(pp, rec, sizeof(rec)); if(ax25_get_info(pp, &pinfo) == 0) return; // Print decoded packet uint32_t out = chsnprintf(buf, len, "%s", rec); for(uint32_t i = 0; pinfo[i]; i++) { if(pinfo[i] < 32 || pinfo[i] > 126) { out += chsnprintf(&buf[out], len - out, "<0x%02x>", pinfo[i]); } else { out += chsnprintf(&buf[out], len - out, "%c", pinfo[i]); } } } /** * @brief Transmit APRS position packet. * * @param[in] callsign origination call sign * @param[in] path path to use * @param[in] symbol symbol for originator * @param[in] dataPoint position data object * * @return encoded packet object pointer * @retval NULL if encoding failed */ packet_t aprs_encode_stamped_position_and_telemetry(const char *callsign, const char *path, aprs_sym_t symbol, dataPoint_t *dataPoint) { // Latitude uint32_t y = 380926 * (90 - dataPoint->gps_lat/10000000.0); uint32_t y3 = y / 753571; uint32_t y3r = y % 753571; uint32_t y2 = y3r / 8281; uint32_t y2r = y3r % 8281; uint32_t y1 = y2r / 91; uint32_t y1r = y2r % 91; // Longitude uint32_t x = 190463 * (180 + dataPoint->gps_lon/10000000.0); uint32_t x3 = x / 753571; uint32_t x3r = x % 753571; uint32_t x2 = x3r / 8281; uint32_t x2r = x3r % 8281; uint32_t x1 = x2r / 91; uint32_t x1r = x2r % 91; // Altitude uint32_t a = logf(METER_TO_FEET(dataPoint->gps_alt)) / logf(1.002f); uint32_t a1 = a / 91; uint32_t a1r = a % 91; uint8_t gpsFix = dataPoint->gps_state == GPS_LOCKED1 || dataPoint->gps_state == GPS_LOCKED2 || dataPoint->gps_state == GPS_FIXED ? GSP_FIX_CURRENT : GSP_FIX_OLD; uint8_t src = NMEA_SRC_GGA; uint8_t origin = ORIGIN_PICO; ptime_t time; getTime(&time); if(time.year == RTC_BASE_YEAR) /* RTC is not set so use dataPoint (it may have a valid date). */ unixTimestamp2Date(&time, dataPoint->gps_time); char xmit[256]; uint32_t len = chsnprintf(xmit, sizeof(xmit), "%s>%s,%s:@%02d%02d%02dz", callsign, APRS_DEVICE_CALLSIGN, path, time.day, time.hour, time.minute); xmit[len+0] = (symbol >> 8) & 0xFF; xmit[len+1] = y3+33; xmit[len+2] = y2+33; xmit[len+3] = y1+33; xmit[len+4] = y1r+33; xmit[len+5] = x3+33; xmit[len+6] = x2+33; xmit[len+7] = x1+33; xmit[len+8] = x1r+33; xmit[len+9] = symbol & 0xFF; xmit[len+10] = a1+33; xmit[len+11] = a1r+33; xmit[len+12] = ((gpsFix << 5) | (src << 3) | origin) + 33; // Comments uint32_t len2 = base91_encode((uint8_t*)dataPoint, (uint8_t*)&xmit[len+13], sizeof(dataPoint_t)); xmit[len+len2+13] = '|'; /* APRS base91 encoded telemetry. */ // Sequence ID uint32_t t = dataPoint->id & 0x1FFF; xmit[len+len2+14] = t/91 + 33; xmit[len+len2+15] = t%91 + 33; // Telemetry analog parameters for(uint8_t i=0; i<5; i++) { switch(i) { case 0: t = dataPoint->adc_vbat; break; case 1: t = dataPoint->adc_vsol; break; case 2: t = dataPoint->pac_pbat+4096; break; case 3: t = dataPoint->sen_i1_temp/10 + 1000; break; case 4: t = dataPoint->sen_i1_press/125 - 40; break; } xmit[len+len2+16+i*2] = t/91 + 33; xmit[len+len2+16+i*2+1] = t%91 + 33; } // Telemetry digital parameter xmit[len+len2+26] = dataPoint->gpio + 33; /* Digital bits second byte - set zero. */ xmit[len+len2+27] = 33; xmit[len+len2+28] = '|'; xmit[len+len2+29] = 0; return ax25_from_text(xmit, true); } /** * @brief Transmit APRS position and telemetry packet. * @notes Base 91 telemetry encoding is used. * @notes The comments are filled with: * @notes - Battery voltage in mV * @notes - Solar voltage in mW (if tracker is solar-enabled) * @notes - Temperature in Celcius * @notes - Air pressure in Pascal * @notes - Number of satellites being used * @notes - Number of cycles where GPS has been lost (if applicable in cycle) * @notes - State of GPIO port(s) * * @param[in] callsign origination call sign * @param[in] path path to use * @param[in] symbol symbol for originator * @param[in] dataPoint position data object * * @return encoded packet object pointer * @retval NULL if encoding failed */ packet_t aprs_encode_position_and_telemetry(const char *callsign, const char *path, aprs_sym_t symbol, dataPoint_t *dataPoint, bool extended) { (void)extended; // Latitude uint32_t y = 380926 * (90 - dataPoint->gps_lat/10000000.0); uint32_t y3 = y / 753571; uint32_t y3r = y % 753571; uint32_t y2 = y3r / 8281; uint32_t y2r = y3r % 8281; uint32_t y1 = y2r / 91; uint32_t y1r = y2r % 91; // Longitude uint32_t x = 190463 * (180 + dataPoint->gps_lon/10000000.0); uint32_t x3 = x / 753571; uint32_t x3r = x % 753571; uint32_t x2 = x3r / 8281; uint32_t x2r = x3r % 8281; uint32_t x1 = x2r / 91; uint32_t x1r = x2r % 91; // Altitude uint32_t a = logf(METER_TO_FEET(dataPoint->gps_alt)) / logf(1.002f); uint32_t a1 = a / 91; uint32_t a1r = a % 91; /* ptime_t time; unixTimestamp2Date(&time, dataPoint->gps_time);*/ char xmit[256]; uint32_t len = chsnprintf(xmit, sizeof(xmit), "%s>%s,%s:=", callsign, APRS_DEVICE_CALLSIGN, path); uint8_t gpsFix = dataPoint->gps_state == GPS_LOCKED1 || dataPoint->gps_state == GPS_LOCKED2 || dataPoint->gps_state == GPS_FIXED ? GSP_FIX_CURRENT : GSP_FIX_OLD; uint8_t src = NMEA_SRC_GGA; uint8_t origin = ORIGIN_PICO; xmit[len+0] = (symbol >> 8) & 0xFF; xmit[len+1] = y3+33; xmit[len+2] = y2+33; xmit[len+3] = y1+33; xmit[len+4] = y1r+33; xmit[len+5] = x3+33; xmit[len+6] = x2+33; xmit[len+7] = x1+33; xmit[len+8] = x1r+33; xmit[len+9] = symbol & 0xFF; xmit[len+10] = a1+33; xmit[len+11] = a1r+33; xmit[len+12] = ((gpsFix << 5) | (src << 3) | origin) + 33; // Comments uint32_t len2 = base91_encode((uint8_t*)dataPoint, (uint8_t*)&xmit[len+13], sizeof(dataPoint_t)); xmit[len+len2+13] = '|'; /* APRS base91 encoded telemetry. */ // Sequence ID uint32_t t = dataPoint->id & 0x1FFF; xmit[len+len2+14] = t/91 + 33; xmit[len+len2+15] = t%91 + 33; // Telemetry analog parameters for(uint8_t i=0; i<5; i++) { switch(i) { case 0: t = dataPoint->adc_vbat; break; case 1: t = dataPoint->adc_vsol; break; case 2: t = dataPoint->pac_pbat+4096; break; case 3: t = dataPoint->sen_i1_temp/10 + 1000; break; case 4: t = dataPoint->sen_i1_press/125 - 40; break; } xmit[len+len2+16+i*2] = t/91 + 33; xmit[len+len2+16+i*2+1] = t%91 + 33; } // Telemetry digital parameter xmit[len+len2+26] = dataPoint->gpio + 33; /* Digital bits second byte - set zero. */ xmit[len+len2+27] = 33; xmit[len+len2+28] = '|'; xmit[len+len2+29] = 0; return ax25_from_text(xmit, true); } /* * */ packet_t aprs_encode_data_packet(const char *callsign, const char *path, char packetType, uint8_t *data) { char xmit[256]; chsnprintf(xmit, sizeof(xmit), "%s>%s,%s:{{%c%s", callsign, APRS_DEVICE_CALLSIGN, path, packetType, data); return ax25_from_text(xmit, true); } /** * @brief Transmit message packet * * @param[in] originator call sign of originator of this message * @param[in path path for message * @param[in] recipient call sign of recipient * @param[in text text of the message * @param[in] ack true if message acknowledgment requested */ packet_t aprs_encode_message(const char *originator, const char *path, const char *recipient, const char *text, const bool ack) { char xmit[256]; if((strlen(text) > AX25_MAX_APRS_MSG_LEN) || (strpbrk(text, "|~{") != NULL)) /* Invalid message. */ return NULL; if(!ack) chsnprintf(xmit, sizeof(xmit), "%s>%s,%s::%-9s:%s", originator, APRS_DEVICE_CALLSIGN, path, recipient, text); else chsnprintf(xmit, sizeof(xmit), "%s>%s,%s::%-9s:%s{%d", originator, APRS_DEVICE_CALLSIGN, path, recipient, text, ++msg_id); return ax25_from_text(xmit, true); } /* * @brief Compose an APRSD message * @notes Used by position service. * * @param[in] originator originator of this message * @param[in] path path to use * @param[in] recipient identity that requested the message */ packet_t aprs_compose_aprsd_message(const char *originator, const char *path, const char *recipient) { char buf[256] = "Directs="; uint32_t out = strlen(buf); uint32_t empty = out; for(uint8_t i = 0; i < APRS_HEARD_LIST_SIZE; i++) { if(heard_list[i].time && heard_list[i].time + TIME_S2I(600) >= chVTGetSystemTime() && heard_list[i].time <= chVTGetSystemTime()) out += chsnprintf(&buf[out], sizeof(buf)-out, "%s ", heard_list[i].call); } if(out == empty) { out += chsnprintf(&buf[out], sizeof(buf)-out, "[none]"); } else { buf[out-1] = 0; // Remove last space } return aprs_encode_message(originator, path, recipient, buf, false); } /* * @brief Encode and send an APRSD message * * @param[in] id aprs node identity * @param[in] argc number of parameters * @param[in] argv array of pointers to parameter strings * * @return result of command * @retval MSG_OK if the command completed. * @retval MSG_ERROR if there was an error. */ msg_t aprs_send_aprsd_message(aprs_identity_t *id, int argc, char *argv[]) { (void)argc; (void)argv; packet_t pp = aprs_compose_aprsd_message(id->call, id->path, id->src); if(pp == NULL) { TRACE_WARN("RX > No free packet objects or badly formed message"); return MSG_ERROR; } if(!transmitOnRadio(pp, id->freq, 0, 0, id->pwr, id->mod, id->cca)) { TRACE_ERROR("RX > Transmit of APRSD failed"); return MSG_ERROR; } return MSG_OK; } /* * @brief Encode and send an APRSH message * * @param[in] id aprs node identity * @param[in] argc number of parameters * @param[in] argv array of pointers to parameter strings * * @return result of command * @retval MSG_OK if the command completed. * @retval MSG_ERROR if there was an error. */ msg_t aprs_send_aprsh_message(aprs_identity_t *id, int argc, char *argv[]) { if(argc != 1) return MSG_ERROR; char buf[AX25_MAX_APRS_MSG_LEN + 1]; uint32_t out = 0; strupr(argv[0]); for(uint8_t i = 0; i < APRS_HEARD_LIST_SIZE; i++) { if(heard_list[i].time && (strncmp(heard_list[i].call, argv[0], strlen(argv[0])) == 0)) { /* Convert time to human readable form. */ time_secs_t diff = chTimeI2S(chVTTimeElapsedSinceX(heard_list[i].time)); out = chsnprintf(buf, sizeof(buf), "%s heard %02i:%02i ago", heard_list[i].call, diff/60, diff % 60); break; } if(out == 0) { out = chsnprintf(buf, sizeof(buf), "%s not heard", argv[0]); } } packet_t pp = aprs_encode_message(id->call, id->path, id->src, buf, false); if(pp == NULL) { TRACE_WARN("RX > No free packet objects or badly formed message"); return MSG_ERROR; } if(!transmitOnRadio(pp, id->freq, 0, 0, id->pwr, id->mod, id->cca)) { TRACE_ERROR("RX > Transmit of APRSH failed"); return MSG_ERROR; } return MSG_OK; } /* * @brief Handle GPIO set/clear/query * * @param[in] id aprs node identity * @param[in] argc number of parameters * @param[in] argv array of pointers to parameter strings * * @return result of command * @retval MSG_OK if the command completed. * @retval MSG_ERROR if there was an error. */ msg_t aprs_execute_gpio_command(aprs_identity_t *id, int argc, char *argv[]) { if(argc != 1) return MSG_ERROR; /* char *tok = strtok(argv[0], ":"); if(tok == NULL) return MSG_ERROR;*/ /* TODO: WIP to generalize by parsing out the port # and operation. */ if(!strcmp(argv[0], "io1:1")) { TRACE_INFO("RX > Message: GPIO set IO1 HIGH"); pktSetGPIOlineMode(LINE_IO1, PAL_MODE_OUTPUT_PUSHPULL); pktWriteGPIOline(LINE_IO1, PAL_HIGH); return MSG_OK; } if(!strcmp(argv[0], "io1:0")) { TRACE_INFO("RX > Message: GPIO set IO1 LOW"); pktSetGPIOlineMode(LINE_IO1, PAL_MODE_OUTPUT_PUSHPULL); pktWriteGPIOline(LINE_IO1, PAL_LOW); return MSG_OK; } if(!strcmp(argv[0], "io2:1")) { TRACE_INFO("RX > Message: GPIO set IO2 HIGH"); pktSetGPIOlineMode(LINE_IO2, PAL_MODE_OUTPUT_PUSHPULL); pktWriteGPIOline(LINE_IO2, PAL_HIGH); return MSG_OK; } if(!strcmp(argv[0], "io2:0")) { TRACE_INFO("RX > Message: GPIO set IO2 LOW"); pktSetGPIOlineMode(LINE_IO2, PAL_MODE_OUTPUT_PUSHPULL); pktWriteGPIOline(LINE_IO2, PAL_LOW); return MSG_OK; } if(!strcmp(argv[0], "io3:1")) { TRACE_INFO("RX > Message: GPIO set IO3 HIGH"); pktSetGPIOlineMode(LINE_IO3, PAL_MODE_OUTPUT_PUSHPULL); pktWriteGPIOline(LINE_IO3, PAL_HIGH); return MSG_OK; } if(!strcmp(argv[0], "io3:0")) { TRACE_INFO("RX > Message: GPIO set IO3 LOW"); pktSetGPIOlineMode(LINE_IO3, PAL_MODE_OUTPUT_PUSHPULL); pktWriteGPIOline(LINE_IO3, PAL_LOW); return MSG_OK; } if(!strcmp(argv[0], "io4:1")) { TRACE_INFO("RX > Message: GPIO set IO4 HIGH"); pktSetGPIOlineMode(LINE_IO4, PAL_MODE_OUTPUT_PUSHPULL); pktWriteGPIOline(LINE_IO4, PAL_HIGH); return MSG_OK; } if(!strcmp(argv[0], "io4:0")) { TRACE_INFO("RX > Message: GPIO set IO4 LOW"); pktSetGPIOlineMode(LINE_IO4, PAL_MODE_OUTPUT_PUSHPULL); pktWriteGPIOline(LINE_IO4, PAL_LOW); return MSG_OK; } /* TODO: Parse out IO number and reduce above and below ugly DRY code. */ packet_t pp; do { if(!strcmp(argv[0], "io1:?")) { char buf[AX25_MAX_APRS_MSG_LEN + 1]; /* TODO: Need to read mode and if not output then report as "input" etc. */ chsnprintf(buf, sizeof(buf), "IO1 is %s ", (pktReadGPIOline(LINE_IO1) == PAL_HIGH) ? "HIGH" : "LOW"); TRACE_INFO("RX > Message: GPIO query IO1 is %s", (pktReadGPIOline(LINE_IO1) == PAL_HIGH) ? "HIGH" : "LOW"); pp = aprs_encode_message(id->call, id->path, id->src, buf, false); if(pp == NULL) { TRACE_WARN("RX > No free packet objects or badly formed message"); return MSG_ERROR; } break; } if(!strcmp(argv[0], "io2:?")) { char buf[AX25_MAX_APRS_MSG_LEN + 1]; /* TODO: Need to read mode and if not output then report as "input" etc. */ chsnprintf(buf, sizeof(buf), "IO2 is %s ", (pktReadGPIOline(LINE_IO2) == PAL_HIGH) ? "HIGH" : "LOW"); TRACE_INFO("RX > Message: GPIO query IO2 is %s", (pktReadGPIOline(LINE_IO2) == PAL_HIGH) ? "HIGH" : "LOW"); pp = aprs_encode_message(id->call, id->path, id->src, buf, false); if(pp == NULL) { TRACE_WARN("RX > No free packet objects or badly formed message"); return MSG_ERROR; } break; } if(!strcmp(argv[0], "io3:?")) { char buf[AX25_MAX_APRS_MSG_LEN + 1]; /* TODO: Need to read mode and if not output then report as "input" etc. */ chsnprintf(buf, sizeof(buf), "IO3 is %s ", (pktReadGPIOline(LINE_IO3) == PAL_HIGH) ? "HIGH" : "LOW"); TRACE_INFO("RX > Message: GPIO query IO3 is %s", (pktReadGPIOline(LINE_IO3) == PAL_HIGH) ? "HIGH" : "LOW"); pp = aprs_encode_message(id->call, id->path, id->src, buf, false); if(pp == NULL) { TRACE_WARN("RX > No free packet objects or badly formed message"); return MSG_ERROR; } break; } if(!strcmp(argv[0], "io4:?")) { char buf[AX25_MAX_APRS_MSG_LEN + 1]; /* TODO: Need to read mode and if not output then report as "input" etc. */ chsnprintf(buf, sizeof(buf), "IO4 is %s ", (pktReadGPIOline(LINE_IO4) == PAL_HIGH) ? "HIGH" : "LOW"); TRACE_INFO("RX > Message: GPIO query IO4 is %s", (pktReadGPIOline(LINE_IO4) == PAL_HIGH) ? "HIGH" : "LOW"); pp = aprs_encode_message(id->call, id->path, id->src, buf, false); if(pp == NULL) { TRACE_WARN("RX > No free packet objects or badly formed message"); return MSG_ERROR; } break; } /* No known IO port found. */ return MSG_ERROR; } while(true); if(!transmitOnRadio(pp, id->freq, 0, 0, id->pwr, id->mod, id->cca)) { TRACE_ERROR("RX > Transmit of GPIO status failed"); return MSG_ERROR; } return MSG_OK; } /** * @brief Request for position beacon to be sent * * @param[in] id aprs node identity * @param[in] argc number of parameters * @param[in] argv array of pointers to parameter strings * * @return result of command * @retval MSG_OK if the command completed. * @retval MSG_ERROR if there was an error. */ msg_t aprs_send_position_response(aprs_identity_t *id, int argc, char *argv[]) { (void)argv; if(argc != 0) return MSG_ERROR; /* * TODO: This should just send a request to the collector service. */ // Encode and transmit telemetry config first for(uint8_t type = 0; type < APRS_NUM_TELEM_GROUPS; type++) { packet_t packet = aprs_encode_telemetry_configuration( id->call, id->path, id->call, type); if(packet == NULL) { TRACE_WARN("BCN > No free packet objects for" " telemetry config transmission"); } else { if(!transmitOnRadio(packet, id->freq, 0, 0, id->pwr, id->mod, id->cca)) { TRACE_ERROR("BCN > Failed to transmit telemetry config"); } } chThdSleep(TIME_S2I(5)); } TRACE_INFO("RX > Message: Position query"); dataPoint_t* dataPoint = getLastDataPoint(); packet_t pp = aprs_encode_stamped_position_and_telemetry(id->call, id->path, id->symbol, dataPoint); if(pp == NULL) { TRACE_WARN("RX > No free packet objects or badly formed message"); return MSG_ERROR; } if(!transmitOnRadio(pp, id->freq, 0, 0, id->pwr, id->mod, id->cca)) { TRACE_ERROR("RX > Transmit of APRSP failed"); return MSG_ERROR; } return MSG_OK; } /** * @brief Request for system reset * * @param[in] id aprs node identity * @param[in] argc number of parameters * @param[in] argv array of pointers to parameter strings * * @return result of command * @retval MSG_ERROR if there was an error. */ msg_t aprs_execute_system_reset(aprs_identity_t *id, int argc, char *argv[]) { (void)argv; if(argc != 0) return MSG_ERROR; TRACE_INFO("RX > Message: System Reset"); char buf[16]; chsnprintf(buf, sizeof(buf), "ack%s", id->num); packet_t pp = aprs_encode_message(id->call, id->path, id->src, buf, false); if(pp == NULL) { TRACE_WARN("RX > No free packet objects"); return MSG_ERROR; } transmitOnRadio(pp, id->freq, 0, 0, id->pwr, id->mod, id->cca); chThdSleep(TIME_S2I(10)); NVIC_SystemReset(); /* We don't arrive here. */ return MSG_OK; } /* * @brief Handle config command * * @param[in] id aprs node identity * @param[in] argc number of parameters * @param[in] argv array of pointers to parameter strings * * @return result of command * @retval MSG_OK if the command completed. * @retval MSG_ERROR if there was an error. */ msg_t aprs_execute_config_command(aprs_identity_t *id, int argc, char *argv[]) { (void)id; if(argc != 2) return MSG_ERROR; for(uint8_t i=0; command_list[i].type != TYPE_NULL; i++) { if(!strncmp(argv[1], command_list[i].name, strlen(command_list[i].name))) { /* Parameter being changed is in argv[0], new value is in argv[1]. */ TRACE_INFO("RX > Message: Configuration Command"); TRACE_INFO("RX > %s => %s", argv[1], argv[1]); if(command_list[i].type == TYPE_INT && command_list[i].size == 1) { *((uint8_t*)command_list[i].ptr) = atoi(argv[1]); } else if(command_list[i].type == TYPE_INT && command_list[i].size == 2) { *((uint16_t*)command_list[i].ptr) = atoi(argv[1]); } else if(command_list[i].type == TYPE_INT && command_list[i].size == 4) { *((uint32_t*)command_list[i].ptr) = atoi(argv[1]); } else if(command_list[i].type == TYPE_TIME) { *((sysinterval_t*)command_list[i].ptr) = TIME_MS2I(atoi(argv[2])); } else if(command_list[i].type == TYPE_STR) { strncpy((char*)command_list[i].ptr, argv[1], sizeof(command_list[i].size)-1); } return MSG_OK; } /* Next parameter. */ } /* Parameter not found. */ return MSG_ERROR; } /** * @brief Request configuration save to flash * * @param[in] id aprs node identity * @param[in] argc number of parameters * @param[in] argv array of pointers to parameter strings * * @return result of command * @retval MSG_ERROR if there was an error. */ msg_t aprs_execute_config_save(aprs_identity_t *id, int argc, char *argv[]) { (void)id; (void)argc; (void)argv; TRACE_INFO("RX > Message: Config Save"); conf_sram.magic = CONFIG_MAGIC_UPDATED; flashSectorBegin(flashSectorAt(0x08060000)); flashErase(0x08060000, 0x20000); flashWrite(0x08060000, (char*)&conf_sram, sizeof(conf_t)); flashSectorEnd(flashSectorAt(0x08060000)); return MSG_OK; } /* * @brief Handle Image command * * @param[in] id aprs node identity * @param[in] argc number of parameters * @param[in] argv array of pointers to parameter strings * * @return result of command * @retval MSG_OK if the command completed. * @retval MSG_ERROR if there was an error. */ msg_t aprs_execute_img_command(aprs_identity_t *id, int argc, char *argv[]) { (void)id; if(argc < 2) return MSG_ERROR; if(!strcmp(argv[0], "reject") && argc == 2) { if(!strcmp(argv[1], "pri")) { reject_pri = true; TRACE_INFO("RX > Message: Image reject pri"); return MSG_OK; } if(!strcmp(argv[1], "sec")) { reject_sec = true; TRACE_INFO("RX > Message: Image reject sec"); return MSG_OK; } return MSG_ERROR; } if(!strcmp(argv[0], "repeat")) { TRACE_INFO("RX > Message: Image packet repeat request"); /* Start at arg 2. */ int c = 2; while(c <= argc) { uint32_t req = strtol(argv[c++], NULL, 16); for(uint8_t i = 0; i < 16; i++) { /* Find an empty repeat slot. */ if(!packetRepeats[i].n_done) { packetRepeats[i].image_id = (req >> 16) & 0xFF; packetRepeats[i].packet_id = req & 0xFFFF; packetRepeats[i].n_done = true; TRACE_INFO("RX > ... Image %3d Packet %3d", packetRepeats[i].image_id, packetRepeats[i].packet_id); break; } /* Not an empty slot. */ } /* No more slots. */ } /* No more image IDs. */ return MSG_OK; } /* Unknown parameter. */ return MSG_ERROR; } /* * @brief Decode APRS content and check for message * * @param[in] pp an APRS packet object * * @return result of command * @retval true if not a message or not addressed to any node on this device. * in that case the APRS content can be digipeated. * @retval false if this was a message for a node on this device. * in that case the APRS content should not be digipeated. */ static bool aprs_decode_message(packet_t pp) { // Get Info field char src[127]; unsigned char *pinfo; if(ax25_get_info(pp, &pinfo) == 0) return false; ax25_format_addrs(pp, src, sizeof(src)); /* Decode destination call sign. */ char dest[AX25_MAX_ADDR_LEN]; uint8_t i = 0; while(i < sizeof(dest) - 1) { if(pinfo[i+1] == ':' || pinfo[i+1] == ' ') { dest[i++] = 0; break; } dest[i] = pinfo[i+1]; i++; } /* Convert destination call sign to upper case. */ strupr(dest); /* Decode source call sign. */ for(uint32_t i = 0; i < sizeof(src); i++) { if(src[i] == '>') { src[i] = 0; break; } } /* Convert source call sign to upper case. */ strupr(src); /* * Setup default responding app identity. * Default identity id set to tx. */ aprs_identity_t identity = {0}; strcpy(identity.src, src); strcpy(identity.call, conf_sram.aprs.tx.call); /* TODO: define a length for path. */ strcpy(identity.path, conf_sram.aprs.tx.path); identity.symbol = conf_sram.aprs.tx.symbol; identity.freq = conf_sram.aprs.tx.radio_conf.freq; identity.pwr = conf_sram.aprs.tx.radio_conf.pwr; identity.mod = conf_sram.aprs.tx.radio_conf.mod; identity.cca = conf_sram.aprs.tx.radio_conf.cca; /* Check which apps are enabled to accept APRS messages. */ bool pos_pri = (strcmp(conf_sram.pos_pri.call, dest) == 0) && (conf_sram.pos_pri.aprs_msg) && (conf_sram.pos_pri.beacon.active); if(pos_pri) { strcpy(identity.call, conf_sram.pos_pri.call); strcpy(identity.path, conf_sram.pos_pri.path); identity.symbol = conf_sram.pos_pri.symbol; } bool pos_sec = (strcmp(conf_sram.pos_sec.call, dest) == 0) && (conf_sram.pos_sec.aprs_msg) && (conf_sram.pos_sec.beacon.active); if(pos_sec) { strcpy(identity.call, conf_sram.pos_sec.call); strcpy(identity.path, conf_sram.pos_sec.path); identity.symbol = conf_sram.pos_sec.symbol; } bool aprs_rx = (strcmp(conf_sram.aprs.rx.call, dest) == 0) && (conf_sram.aprs.rx.svc_conf.active); if(aprs_rx) { strcpy(identity.call, conf_sram.aprs.rx.call); identity.symbol = conf_sram.aprs.rx.symbol; /* Other parameters come from tx identity. */ } /* Even if the digi is not active respond on the digi (TX) call. */ bool aprs_tx = (strcmp(conf_sram.aprs.tx.call, dest) == 0) && (conf_sram.aprs.rx.svc_conf.active) /*&& (conf_sram.aprs.digi.active)*/; /* Default already set tx parameters. */ /* Check if this is message and address is one of the apps on this device. */ if(!((pinfo[10] == ':') && (pos_pri || pos_sec || aprs_rx || aprs_tx))) { /* * Not a command or not addressed to one of the active apps on this device. * Flag that message should be digipeated. */ return true; } /* Proceed with command analysis. */ char msg_id_rx[8] = {0}; //memset(msg_id_rx, 0, sizeof(msg_id_rx)); // Cut off control chars for(uint16_t i = 11; pinfo[i] != 0 && i < (AX25_MAX_APRS_MSG_LEN + 11); i++) { /* FIXME: Trim trailing spaces before {. */ if(pinfo[i] == '{') { // Copy ACK ID memcpy(msg_id_rx, &pinfo[i+1], sizeof(msg_id_rx)-1); // Cut off non-printable chars for(uint8_t j=0; j 126) { msg_id_rx[j] = 0; break; } } pinfo[i] = 0; // Mark end of message } if(pinfo[i] == '\r' || pinfo[i] == '\n') { pinfo[i] = 0; } } strcpy(identity.num, msg_id_rx); /* Convert command string to lower case. */ char *astrng = strlwr((char*)&pinfo[11]); // Trace TRACE_INFO("RX > Received message from %s (ID=%s): %s", src, msg_id_rx[0] == 0 ? "none" : msg_id_rx, astrng); /* Filter out telemetry configuration and "Directs=" messages sent to ourselves. */ char const *cfgs[] = {"parm.", "unit.", "eqns.", "bits.", "directs="}; uint8_t x; for(x = 0; x < (sizeof(cfgs) / sizeof((cfgs)[0])); x++) { if(strncmp(astrng, cfgs[x], strlen(cfgs[x])) == 0) return false; } /* Parse arguments. */ char *lp, *cmd, *tokp; char *args[APRS_MAX_MSG_ARGUMENTS]; lp = aprs_parse_arguments(astrng, &tokp); /* The command itself. */ cmd = lp; int n = 0; while ((lp = aprs_parse_arguments(NULL, &tokp)) != NULL) { if (n >= APRS_MAX_MSG_ARGUMENTS) { TRACE_INFO("RX > Too many APRS command arguments"); cmd = NULL; break; } args[n++] = lp; } /* Parse and execute command. */ msg_t msg = aprs_cmd_exec(aprs_commands, cmd, &identity, n, args); if(msg == MSG_TIMEOUT) { TRACE_INFO("RX > No command found in message"); } if(msg_id_rx[0]) { /* Incoming message ID exists so an ACK or REJ has to be sent. */ char buf[16]; chsnprintf(buf, sizeof(buf), "%s%s", (msg == MSG_OK || msg == MSG_TIMEOUT) ? "ack" : "rej", msg_id_rx); /* * Use the receiving node identity as sender. * Don't request acknowledgment. */ packet_t pp = aprs_encode_message(identity.call, identity.path, identity.src, buf, false); if(pp == NULL) { TRACE_WARN("RX > No free packet objects"); return false; } transmitOnRadio(pp, identity.freq, 0, 0, identity.pwr, identity.mod, identity.cca); } /* Flag that the APRS content should not be digipeated. */ return false; } /** * Transmit failure will release the packet memory. */ static void aprs_digipeat(packet_t pp) { if(!dedupe_initialized) { dedupe_init(TIME_S2I(10)); dedupe_initialized = true; } if(!dedupe_check(pp, 0)) { // Last identical packet older than 10 seconds packet_t result = digipeat_match(0, pp, conf_sram.aprs.rx.call, conf_sram.aprs.tx.call, alias_re, wide_re, 0, preempt, NULL); if(result != NULL) { // Should be digipeated dedupe_remember(result, 0); /* If transmit fails the packet buffer is released. */ if(!transmitOnRadio(result, conf_sram.aprs.tx.radio_conf.freq, 0, 0, conf_sram.aprs.tx.radio_conf.pwr, conf_sram.aprs.tx.radio_conf.mod, conf_sram.aprs.tx.radio_conf.cca)) { TRACE_INFO("RX > Failed to digipeat packet"); } /* TX failed. */ } /* Should be digipeated. */ } /* Duplicate check. */ } /** * Transmit APRS telemetry configuration */ packet_t aprs_encode_telemetry_configuration(const char *originator, const char *path, const char *destination, uint8_t type) { switch(type) { case 0: return aprs_encode_message(originator, path, destination, "PARM.Vbat,Vsol,Pbat,Temperature,Airpressure," "IO1,IO2,IO3,IO4", false); case 1: return aprs_encode_message(originator, path, destination, "UNIT.V,V,W,degC,Pa,1,1,1,1", false); case 2: return aprs_encode_message(originator, path, destination, "EQNS.0,0.001,0,0,0.001,0,0,0.001," "-4.096,0,0.1,-100,0,12.5,500", false); case 3: return aprs_encode_message(originator, path, destination, "BITS.1111,Pecan Pico", false); default: return NULL; } } /* * */ void aprs_decode_packet(packet_t pp) { // Get heard callsign char call[AX25_MAX_ADDR_LEN]; int8_t v = -1; do { v++; ax25_get_addr_with_ssid(pp, ax25_get_heard(pp)-v, call); } while(((ax25_get_heard(pp) - v) >= AX25_SOURCE) && (!strncmp("WIDE", call, 4) || !strncmp("TRACE", call, 5))); // Fill/Update direct list sysinterval_t first_time = 0xFFFFFFFF; // Timestamp of oldest heard list entry uint8_t first_id = 0; // ID of oldest heard list entry for(uint8_t i=0; i <= APRS_HEARD_LIST_SIZE; i++) { if(i < APRS_HEARD_LIST_SIZE) { // Search for callsign in list if(!strcmp(heard_list[i].call, call)) { // Callsign found in list heard_list[i].time = chVTGetSystemTime(); // Update time the callsign was last heard break; } // Find oldest entry if(first_time > heard_list[i].time) { first_time = heard_list[i].time; first_id = i; } } else { // Callsign not in list // Overwrite old entry/ use empty entry memcpy(heard_list[first_id].call, call, sizeof(heard_list[first_id].call)); heard_list[first_id].time = chVTGetSystemTime(); } } // Decode message packets bool digipeat = true; unsigned char *pinfo; if(ax25_get_info(pp, &pinfo) == 0) return; /* * Check if the message is for us. * Execute any command found in the message. * If not then digipeat it. */ if(pinfo[0] == ':') { digipeat = aprs_decode_message(pp); // ax25_get_dti(pp) } // Digipeat packet if(conf_sram.aprs.digi && digipeat) { aprs_digipeat(pp); } }