mirror of
https://github.com/RFnexus/modem73.git
synced 2026-04-27 14:30:33 +00:00
CSMA sample fix, 8073 TCP Control port layer
This commit is contained in:
parent
15d622f82e
commit
67f5e0aba3
10 changed files with 4344 additions and 107 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -1,2 +1,4 @@
|
|||
miniaudio.o
|
||||
modem73
|
||||
modem73
|
||||
TODO
|
||||
test-dual.sh
|
||||
9
Makefile
9
Makefile
|
|
@ -13,8 +13,8 @@ INCLUDES = -I$(AICODIX_DSP) -I$(AICODIX_CODE) -I$(MODEM_SRC)
|
|||
TARGET = modem73
|
||||
|
||||
SRCS = kiss_tnc.cc
|
||||
HDRS = kiss_tnc.hh miniaudio_audio.hh rigctl_ptt.hh modem.hh tnc_ui.hh
|
||||
OBJS = miniaudio.o
|
||||
HDRS = kiss_tnc.hh miniaudio_audio.hh rigctl_ptt.hh modem.hh tnc_ui.hh control_port.hh
|
||||
OBJS = miniaudio.o cJSON.o
|
||||
|
||||
# defualt to build with UI, headless operations through --headless
|
||||
UI_FLAGS = -DWITH_UI
|
||||
|
|
@ -40,6 +40,9 @@ all: $(TARGET)
|
|||
miniaudio.o: miniaudio.c miniaudio.h
|
||||
$(CC) -c -O2 -o $@ miniaudio.c
|
||||
|
||||
cJSON.o: cJSON.c cJSON.h
|
||||
$(CC) -c -O2 -o $@ cJSON.c
|
||||
|
||||
$(TARGET): $(SRCS) $(HDRS) $(OBJS)
|
||||
$(CXX) $(CXXFLAGS) $(UI_FLAGS) $(CM108_FLAGS) $(INCLUDES) -o $@ $(SRCS) $(OBJS) $(LDFLAGS)
|
||||
ifneq ($(HIDAPI_LIBS),)
|
||||
|
|
@ -50,7 +53,7 @@ ifneq ($(HIDAPI_LIBS),)
|
|||
endif
|
||||
|
||||
clean:
|
||||
rm -f $(TARGET) $(OBJS)
|
||||
rm -f $(TARGET) $(OBJS) cJSON.o
|
||||
|
||||
install: $(TARGET)
|
||||
install -m 755 $(TARGET) /usr/local/bin/
|
||||
|
|
|
|||
306
cJSON.h
Normal file
306
cJSON.h
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
/*
|
||||
Copyright (c) 2009-2017 Dave Gamble and cJSON contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef cJSON__h
|
||||
#define cJSON__h
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C"
|
||||
{
|
||||
#endif
|
||||
|
||||
#if !defined(__WINDOWS__) && (defined(WIN32) || defined(WIN64) || defined(_MSC_VER) || defined(_WIN32))
|
||||
#define __WINDOWS__
|
||||
#endif
|
||||
|
||||
#ifdef __WINDOWS__
|
||||
|
||||
/* When compiling for windows, we specify a specific calling convention to avoid issues where we are being called from a project with a different default calling convention. For windows you have 3 define options:
|
||||
|
||||
CJSON_HIDE_SYMBOLS - Define this in the case where you don't want to ever dllexport symbols
|
||||
CJSON_EXPORT_SYMBOLS - Define this on library build when you want to dllexport symbols (default)
|
||||
CJSON_IMPORT_SYMBOLS - Define this if you want to dllimport symbol
|
||||
|
||||
For *nix builds that support visibility attribute, you can define similar behavior by
|
||||
|
||||
setting default visibility to hidden by adding
|
||||
-fvisibility=hidden (for gcc)
|
||||
or
|
||||
-xldscope=hidden (for sun cc)
|
||||
to CFLAGS
|
||||
|
||||
then using the CJSON_API_VISIBILITY flag to "export" the same symbols the way CJSON_EXPORT_SYMBOLS does
|
||||
|
||||
*/
|
||||
|
||||
#define CJSON_CDECL __cdecl
|
||||
#define CJSON_STDCALL __stdcall
|
||||
|
||||
/* export symbols by default, this is necessary for copy pasting the C and header file */
|
||||
#if !defined(CJSON_HIDE_SYMBOLS) && !defined(CJSON_IMPORT_SYMBOLS) && !defined(CJSON_EXPORT_SYMBOLS)
|
||||
#define CJSON_EXPORT_SYMBOLS
|
||||
#endif
|
||||
|
||||
#if defined(CJSON_HIDE_SYMBOLS)
|
||||
#define CJSON_PUBLIC(type) type CJSON_STDCALL
|
||||
#elif defined(CJSON_EXPORT_SYMBOLS)
|
||||
#define CJSON_PUBLIC(type) __declspec(dllexport) type CJSON_STDCALL
|
||||
#elif defined(CJSON_IMPORT_SYMBOLS)
|
||||
#define CJSON_PUBLIC(type) __declspec(dllimport) type CJSON_STDCALL
|
||||
#endif
|
||||
#else /* !__WINDOWS__ */
|
||||
#define CJSON_CDECL
|
||||
#define CJSON_STDCALL
|
||||
|
||||
#if (defined(__GNUC__) || defined(__SUNPRO_CC) || defined (__SUNPRO_C)) && defined(CJSON_API_VISIBILITY)
|
||||
#define CJSON_PUBLIC(type) __attribute__((visibility("default"))) type
|
||||
#else
|
||||
#define CJSON_PUBLIC(type) type
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/* project version */
|
||||
#define CJSON_VERSION_MAJOR 1
|
||||
#define CJSON_VERSION_MINOR 7
|
||||
#define CJSON_VERSION_PATCH 19
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/* cJSON Types: */
|
||||
#define cJSON_Invalid (0)
|
||||
#define cJSON_False (1 << 0)
|
||||
#define cJSON_True (1 << 1)
|
||||
#define cJSON_NULL (1 << 2)
|
||||
#define cJSON_Number (1 << 3)
|
||||
#define cJSON_String (1 << 4)
|
||||
#define cJSON_Array (1 << 5)
|
||||
#define cJSON_Object (1 << 6)
|
||||
#define cJSON_Raw (1 << 7) /* raw json */
|
||||
|
||||
#define cJSON_IsReference 256
|
||||
#define cJSON_StringIsConst 512
|
||||
|
||||
/* The cJSON structure: */
|
||||
typedef struct cJSON
|
||||
{
|
||||
/* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */
|
||||
struct cJSON *next;
|
||||
struct cJSON *prev;
|
||||
/* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */
|
||||
struct cJSON *child;
|
||||
|
||||
/* The type of the item, as above. */
|
||||
int type;
|
||||
|
||||
/* The item's string, if type==cJSON_String and type == cJSON_Raw */
|
||||
char *valuestring;
|
||||
/* writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead */
|
||||
int valueint;
|
||||
/* The item's number, if type==cJSON_Number */
|
||||
double valuedouble;
|
||||
|
||||
/* The item's name string, if this item is the child of, or is in the list of subitems of an object. */
|
||||
char *string;
|
||||
} cJSON;
|
||||
|
||||
typedef struct cJSON_Hooks
|
||||
{
|
||||
/* malloc/free are CDECL on Windows regardless of the default calling convention of the compiler, so ensure the hooks allow passing those functions directly. */
|
||||
void *(CJSON_CDECL *malloc_fn)(size_t sz);
|
||||
void (CJSON_CDECL *free_fn)(void *ptr);
|
||||
} cJSON_Hooks;
|
||||
|
||||
typedef int cJSON_bool;
|
||||
|
||||
/* Limits how deeply nested arrays/objects can be before cJSON rejects to parse them.
|
||||
* This is to prevent stack overflows. */
|
||||
#ifndef CJSON_NESTING_LIMIT
|
||||
#define CJSON_NESTING_LIMIT 1000
|
||||
#endif
|
||||
|
||||
/* Limits the length of circular references can be before cJSON rejects to parse them.
|
||||
* This is to prevent stack overflows. */
|
||||
#ifndef CJSON_CIRCULAR_LIMIT
|
||||
#define CJSON_CIRCULAR_LIMIT 10000
|
||||
#endif
|
||||
|
||||
/* returns the version of cJSON as a string */
|
||||
CJSON_PUBLIC(const char*) cJSON_Version(void);
|
||||
|
||||
/* Supply malloc, realloc and free functions to cJSON */
|
||||
CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks);
|
||||
|
||||
/* Memory Management: the caller is always responsible to free the results from all variants of cJSON_Parse (with cJSON_Delete) and cJSON_Print (with stdlib free, cJSON_Hooks.free_fn, or cJSON_free as appropriate). The exception is cJSON_PrintPreallocated, where the caller has full responsibility of the buffer. */
|
||||
/* Supply a block of JSON, and this returns a cJSON object you can interrogate. */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length);
|
||||
/* ParseWithOpts allows you to require (and check) that the JSON is null terminated, and to retrieve the pointer to the final byte parsed. */
|
||||
/* If you supply a ptr in return_parse_end and parsing fails, then return_parse_end will contain a pointer to the error so will match cJSON_GetErrorPtr(). */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated);
|
||||
|
||||
/* Render a cJSON entity to text for transfer/storage. */
|
||||
CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item);
|
||||
/* Render a cJSON entity to text for transfer/storage without any formatting. */
|
||||
CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item);
|
||||
/* Render a cJSON entity to text using a buffered strategy. prebuffer is a guess at the final size. guessing well reduces reallocation. fmt=0 gives unformatted, =1 gives formatted */
|
||||
CJSON_PUBLIC(char *) cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt);
|
||||
/* Render a cJSON entity to text using a buffer already allocated in memory with given length. Returns 1 on success and 0 on failure. */
|
||||
/* NOTE: cJSON is not always 100% accurate in estimating how much memory it will use, so to be safe allocate 5 bytes more than you actually need */
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format);
|
||||
/* Delete a cJSON entity and all subentities. */
|
||||
CJSON_PUBLIC(void) cJSON_Delete(cJSON *item);
|
||||
|
||||
/* Returns the number of items in an array (or object). */
|
||||
CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON *array);
|
||||
/* Retrieve item number "index" from array "array". Returns NULL if unsuccessful. */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_GetArrayItem(const cJSON *array, int index);
|
||||
/* Get item "string" from object. Case insensitive. */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_GetObjectItemCaseSensitive(const cJSON * const object, const char * const string);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON *object, const char *string);
|
||||
/* For analysing failed parses. This returns a pointer to the parse error. You'll probably need to look a few chars back to make sense of it. Defined when cJSON_Parse() returns 0. 0 when cJSON_Parse() succeeds. */
|
||||
CJSON_PUBLIC(const char *) cJSON_GetErrorPtr(void);
|
||||
|
||||
/* Check item type and return its value */
|
||||
CJSON_PUBLIC(char *) cJSON_GetStringValue(const cJSON * const item);
|
||||
CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON * const item);
|
||||
|
||||
/* These functions check the type of an item */
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsInvalid(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsFalse(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsTrue(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsBool(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsNull(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsNumber(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsString(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsArray(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsObject(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON * const item);
|
||||
|
||||
/* These calls create a cJSON item of the appropriate type. */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateNull(void);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateTrue(void);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateFalse(void);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateBool(cJSON_bool boolean);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateNumber(double num);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateString(const char *string);
|
||||
/* raw json */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateRaw(const char *raw);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateArray(void);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateObject(void);
|
||||
|
||||
/* Create a string where valuestring references a string so
|
||||
* it will not be freed by cJSON_Delete */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateStringReference(const char *string);
|
||||
/* Create an object/array that only references it's elements so
|
||||
* they will not be freed by cJSON_Delete */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateObjectReference(const cJSON *child);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateArrayReference(const cJSON *child);
|
||||
|
||||
/* These utilities create an Array of count items.
|
||||
* The parameter count cannot be greater than the number of elements in the number array, otherwise array access will be out of bounds.*/
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateIntArray(const int *numbers, int count);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateFloatArray(const float *numbers, int count);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateDoubleArray(const double *numbers, int count);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateStringArray(const char *const *strings, int count);
|
||||
|
||||
/* Append item to the specified array/object. */
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON *array, cJSON *item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item);
|
||||
/* Use this when string is definitely const (i.e. a literal, or as good as), and will definitely survive the cJSON object.
|
||||
* WARNING: When this function was used, make sure to always check that (item->type & cJSON_StringIsConst) is zero before
|
||||
* writing to `item->string` */
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObjectCS(cJSON *object, const char *string, cJSON *item);
|
||||
/* Append reference to item to the specified array/object. Use this when you want to add an existing cJSON to a new cJSON, but don't want to corrupt your existing cJSON. */
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToObject(cJSON *object, const char *string, cJSON *item);
|
||||
|
||||
/* Remove/Detach items from Arrays/Objects. */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_DetachItemViaPointer(cJSON *parent, cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromArray(cJSON *array, int which);
|
||||
CJSON_PUBLIC(void) cJSON_DeleteItemFromArray(cJSON *array, int which);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObject(cJSON *object, const char *string);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObjectCaseSensitive(cJSON *object, const char *string);
|
||||
CJSON_PUBLIC(void) cJSON_DeleteItemFromObject(cJSON *object, const char *string);
|
||||
CJSON_PUBLIC(void) cJSON_DeleteItemFromObjectCaseSensitive(cJSON *object, const char *string);
|
||||
|
||||
/* Update array items. */
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_InsertItemInArray(cJSON *array, int which, cJSON *newitem); /* Shifts pre-existing items to the right. */
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemViaPointer(cJSON * const parent, cJSON * const item, cJSON * replacement);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInArray(cJSON *array, int which, cJSON *newitem);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObject(cJSON *object,const char *string,cJSON *newitem);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObjectCaseSensitive(cJSON *object,const char *string,cJSON *newitem);
|
||||
|
||||
/* Duplicate a cJSON item */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_Duplicate(const cJSON *item, cJSON_bool recurse);
|
||||
/* Duplicate will create a new, identical cJSON item to the one you pass, in new memory that will
|
||||
* need to be released. With recurse!=0, it will duplicate any children connected to the item.
|
||||
* The item->next and ->prev pointers are always zero on return from Duplicate. */
|
||||
/* Recursively compare two cJSON items for equality. If either a or b is NULL or invalid, they will be considered unequal.
|
||||
* case_sensitive determines if object keys are treated case sensitive (1) or case insensitive (0) */
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive);
|
||||
|
||||
/* Minify a strings, remove blank characters(such as ' ', '\t', '\r', '\n') from strings.
|
||||
* The input pointer json cannot point to a read-only address area, such as a string constant,
|
||||
* but should point to a readable and writable address area. */
|
||||
CJSON_PUBLIC(void) cJSON_Minify(char *json);
|
||||
|
||||
/* Helper functions for creating and adding items to an object at the same time.
|
||||
* They return the added item or NULL on failure. */
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON * const object, const char * const name);
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON * const object, const char * const name);
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON * const object, const char * const name);
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean);
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number);
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string);
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw);
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON * const object, const char * const name);
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON * const object, const char * const name);
|
||||
|
||||
/* When assigning an integer value, it needs to be propagated to valuedouble too. */
|
||||
#define cJSON_SetIntValue(object, number) ((object) ? (object)->valueint = (object)->valuedouble = (number) : (number))
|
||||
/* helper for the cJSON_SetNumberValue macro */
|
||||
CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON *object, double number);
|
||||
#define cJSON_SetNumberValue(object, number) ((object != NULL) ? cJSON_SetNumberHelper(object, (double)number) : (number))
|
||||
/* Change the valuestring of a cJSON_String object, only takes effect when type of object is cJSON_String */
|
||||
CJSON_PUBLIC(char*) cJSON_SetValuestring(cJSON *object, const char *valuestring);
|
||||
|
||||
/* If the object is not a boolean type this does nothing and returns cJSON_Invalid else it returns the new type*/
|
||||
#define cJSON_SetBoolValue(object, boolValue) ( \
|
||||
(object != NULL && ((object)->type & (cJSON_False|cJSON_True))) ? \
|
||||
(object)->type=((object)->type &(~(cJSON_False|cJSON_True)))|((boolValue)?cJSON_True:cJSON_False) : \
|
||||
cJSON_Invalid\
|
||||
)
|
||||
|
||||
/* Macro for iterating over an array or object */
|
||||
#define cJSON_ArrayForEach(element, array) for(element = (array != NULL) ? (array)->child : NULL; element != NULL; element = element->next)
|
||||
|
||||
/* malloc/free objects using the malloc/free functions that have been set with cJSON_InitHooks */
|
||||
CJSON_PUBLIC(void *) cJSON_malloc(size_t size);
|
||||
CJSON_PUBLIC(void) cJSON_free(void *object);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
426
control_port.hh
Normal file
426
control_port.hh
Normal file
|
|
@ -0,0 +1,426 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
#include <list>
|
||||
#include <mutex>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <poll.h>
|
||||
|
||||
extern "C" {
|
||||
#include "cJSON.h"
|
||||
}
|
||||
|
||||
// Base64 decode (RFC 4648)
|
||||
inline std::vector<uint8_t> base64_decode(const char* input, size_t len) {
|
||||
static const uint8_t T[256] = {
|
||||
64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,
|
||||
64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,
|
||||
64,64,64,64,64,64,64,64,64,64,64,62,64,64,64,63,
|
||||
52,53,54,55,56,57,58,59,60,61,64,64,64,65,64,64,
|
||||
64, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,
|
||||
15,16,17,18,19,20,21,22,23,24,25,64,64,64,64,64,
|
||||
64,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,
|
||||
41,42,43,44,45,46,47,48,49,50,51,64,64,64,64,64,
|
||||
64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,
|
||||
64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,
|
||||
64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,
|
||||
64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,
|
||||
64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,
|
||||
64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,
|
||||
64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,
|
||||
64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,
|
||||
};
|
||||
std::vector<uint8_t> out;
|
||||
out.reserve(len * 3 / 4);
|
||||
uint32_t buf = 0;
|
||||
int bits = 0;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
uint8_t v = T[(uint8_t)input[i]];
|
||||
if (v >= 64) continue; // skip padding and invalid
|
||||
buf = (buf << 6) | v;
|
||||
bits += 6;
|
||||
if (bits >= 8) {
|
||||
bits -= 8;
|
||||
out.push_back((buf >> bits) & 0xFF);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
class ControlPort {
|
||||
public:
|
||||
struct TNCInterface {
|
||||
std::function<cJSON*()> get_status;
|
||||
std::function<cJSON*()> get_config;
|
||||
std::function<bool(cJSON* params)> set_config;
|
||||
std::function<std::string(const std::string&)> rigctl_command;
|
||||
std::function<bool(const std::vector<uint8_t>&, int oper_mode)> tx_data;
|
||||
};
|
||||
|
||||
ControlPort(int port, const std::string& bind_address, TNCInterface iface)
|
||||
: port_(port), bind_address_(bind_address), iface_(std::move(iface)) {}
|
||||
|
||||
~ControlPort() {
|
||||
stop();
|
||||
}
|
||||
|
||||
bool start() {
|
||||
server_fd_ = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (server_fd_ < 0) {
|
||||
std::cerr << "control: Failed to create socket" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
int opt = 1;
|
||||
setsockopt(server_fd_, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
|
||||
|
||||
struct sockaddr_in addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_addr.s_addr = inet_addr(bind_address_.c_str());
|
||||
addr.sin_port = htons(port_);
|
||||
|
||||
if (bind(server_fd_, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
|
||||
std::cerr << "control: Failed to bind to port " << port_ << std::endl;
|
||||
close(server_fd_);
|
||||
server_fd_ = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (listen(server_fd_, 5) < 0) {
|
||||
std::cerr << "control: Failed to listen" << std::endl;
|
||||
close(server_fd_);
|
||||
server_fd_ = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
fcntl(server_fd_, F_SETFL, O_NONBLOCK);
|
||||
|
||||
running_ = true;
|
||||
thread_ = std::thread(&ControlPort::run, this);
|
||||
|
||||
std::cerr << "Control port listening on " << bind_address_ << ":" << port_ << std::endl;
|
||||
return true;
|
||||
}
|
||||
|
||||
void stop() {
|
||||
running_ = false;
|
||||
if (thread_.joinable()) {
|
||||
thread_.join();
|
||||
}
|
||||
if (server_fd_ >= 0) {
|
||||
close(server_fd_);
|
||||
server_fd_ = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Push a config_changed event to all connected clients
|
||||
void notify_config_changed() {
|
||||
if (!iface_.get_config) return;
|
||||
cJSON* config = iface_.get_config();
|
||||
if (!config) return;
|
||||
|
||||
cJSON* event = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(event, "event", "config_changed");
|
||||
cJSON_AddItemToObject(event, "config", config);
|
||||
broadcast_event(event);
|
||||
cJSON_Delete(event);
|
||||
}
|
||||
|
||||
// Push an event to all connected clients (thread-safe)
|
||||
void broadcast_event(cJSON* event) {
|
||||
char* str = cJSON_PrintUnformatted(event);
|
||||
if (!str) return;
|
||||
|
||||
uint32_t len = strlen(str);
|
||||
uint8_t header[4];
|
||||
header[0] = (len >> 24) & 0xFF;
|
||||
header[1] = (len >> 16) & 0xFF;
|
||||
header[2] = (len >> 8) & 0xFF;
|
||||
header[3] = len & 0xFF;
|
||||
|
||||
std::lock_guard<std::recursive_mutex> lock(clients_mutex_);
|
||||
for (auto& client : clients_) {
|
||||
::send(client.fd, header, 4, MSG_NOSIGNAL);
|
||||
::send(client.fd, str, len, MSG_NOSIGNAL);
|
||||
}
|
||||
|
||||
cJSON_free(str);
|
||||
}
|
||||
|
||||
private:
|
||||
struct Client {
|
||||
int fd;
|
||||
std::vector<uint8_t> recv_buf;
|
||||
|
||||
Client(int fd) : fd(fd) {}
|
||||
};
|
||||
|
||||
void run() {
|
||||
while (running_) {
|
||||
// Accept new connections
|
||||
struct sockaddr_in client_addr;
|
||||
socklen_t client_len = sizeof(client_addr);
|
||||
int client_fd = accept(server_fd_, (struct sockaddr*)&client_addr, &client_len);
|
||||
|
||||
if (client_fd >= 0) {
|
||||
int flag = 1;
|
||||
setsockopt(client_fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));
|
||||
fcntl(client_fd, F_SETFL, O_NONBLOCK);
|
||||
|
||||
char ip_str[INET_ADDRSTRLEN];
|
||||
inet_ntop(AF_INET, &client_addr.sin_addr, ip_str, sizeof(ip_str));
|
||||
std::cerr << "control: Client connected from " << ip_str << std::endl;
|
||||
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(clients_mutex_);
|
||||
clients_.emplace_back(client_fd);
|
||||
}
|
||||
}
|
||||
|
||||
// Poll clients
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(clients_mutex_);
|
||||
for (auto it = clients_.begin(); it != clients_.end();) {
|
||||
uint8_t buf[4096];
|
||||
ssize_t n = recv(it->fd, buf, sizeof(buf), MSG_DONTWAIT);
|
||||
|
||||
if (n > 0) {
|
||||
it->recv_buf.insert(it->recv_buf.end(), buf, buf + n);
|
||||
process_recv_buf(*it);
|
||||
} else if (n == 0 || (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK)) {
|
||||
std::cerr << "control: Client disconnected" << std::endl;
|
||||
close(it->fd);
|
||||
it = clients_.erase(it);
|
||||
continue;
|
||||
}
|
||||
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(clients_mutex_);
|
||||
for (auto& client : clients_) {
|
||||
close(client.fd);
|
||||
}
|
||||
clients_.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void process_recv_buf(Client& client) {
|
||||
// Messages are: [4-byte big-endian length][JSON bytes]
|
||||
while (client.recv_buf.size() >= 4) {
|
||||
uint32_t msg_len = ((uint32_t)client.recv_buf[0] << 24) |
|
||||
((uint32_t)client.recv_buf[1] << 16) |
|
||||
((uint32_t)client.recv_buf[2] << 8) |
|
||||
((uint32_t)client.recv_buf[3]);
|
||||
|
||||
if (msg_len > 1024 * 1024) {
|
||||
// Sanity limit: 1MB
|
||||
std::cerr << "control: Message too large (" << msg_len << "), disconnecting" << std::endl;
|
||||
client.recv_buf.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (client.recv_buf.size() < 4 + msg_len) {
|
||||
break; // Need more data
|
||||
}
|
||||
|
||||
// Extract JSON
|
||||
std::string json_str(client.recv_buf.begin() + 4,
|
||||
client.recv_buf.begin() + 4 + msg_len);
|
||||
client.recv_buf.erase(client.recv_buf.begin(),
|
||||
client.recv_buf.begin() + 4 + msg_len);
|
||||
|
||||
handle_message(client.fd, json_str);
|
||||
}
|
||||
}
|
||||
|
||||
void handle_message(int client_fd, const std::string& json_str) {
|
||||
cJSON* request = cJSON_Parse(json_str.c_str());
|
||||
if (!request) {
|
||||
send_error(client_fd, "invalid JSON");
|
||||
return;
|
||||
}
|
||||
|
||||
cJSON* cmd = cJSON_GetObjectItemCaseSensitive(request, "cmd");
|
||||
if (!cJSON_IsString(cmd) || !cmd->valuestring) {
|
||||
send_error(client_fd, "missing 'cmd' field");
|
||||
cJSON_Delete(request);
|
||||
return;
|
||||
}
|
||||
|
||||
const char* cmd_str = cmd->valuestring;
|
||||
|
||||
if (strcmp(cmd_str, "get_status") == 0) {
|
||||
handle_get_status(client_fd);
|
||||
} else if (strcmp(cmd_str, "get_config") == 0) {
|
||||
handle_get_config(client_fd);
|
||||
} else if (strcmp(cmd_str, "set_config") == 0) {
|
||||
handle_set_config(client_fd, request);
|
||||
} else if (strcmp(cmd_str, "rigctl") == 0) {
|
||||
handle_rigctl(client_fd, request);
|
||||
} else if (strcmp(cmd_str, "tx") == 0) {
|
||||
handle_tx(client_fd, request);
|
||||
} else {
|
||||
send_error(client_fd, "unknown command");
|
||||
}
|
||||
|
||||
cJSON_Delete(request);
|
||||
}
|
||||
|
||||
void handle_get_status(int client_fd) {
|
||||
if (!iface_.get_status) {
|
||||
send_error(client_fd, "get_status not available");
|
||||
return;
|
||||
}
|
||||
cJSON* response = iface_.get_status();
|
||||
if (response) {
|
||||
cJSON_AddBoolToObject(response, "ok", 1);
|
||||
send_json(client_fd, response);
|
||||
cJSON_Delete(response);
|
||||
} else {
|
||||
send_error(client_fd, "internal error");
|
||||
}
|
||||
}
|
||||
|
||||
void handle_get_config(int client_fd) {
|
||||
if (!iface_.get_config) {
|
||||
send_error(client_fd, "get_config not available");
|
||||
return;
|
||||
}
|
||||
cJSON* response = iface_.get_config();
|
||||
if (response) {
|
||||
cJSON_AddBoolToObject(response, "ok", 1);
|
||||
send_json(client_fd, response);
|
||||
cJSON_Delete(response);
|
||||
} else {
|
||||
send_error(client_fd, "internal error");
|
||||
}
|
||||
}
|
||||
|
||||
void handle_set_config(int client_fd, cJSON* request) {
|
||||
if (!iface_.set_config) {
|
||||
send_error(client_fd, "set_config not available");
|
||||
return;
|
||||
}
|
||||
if (iface_.set_config(request)) {
|
||||
cJSON* response = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(response, "ok", 1);
|
||||
send_json(client_fd, response);
|
||||
cJSON_Delete(response);
|
||||
notify_config_changed();
|
||||
} else {
|
||||
send_error(client_fd, "set_config failed");
|
||||
}
|
||||
}
|
||||
|
||||
void handle_rigctl(int client_fd, cJSON* request) {
|
||||
if (!iface_.rigctl_command) {
|
||||
send_error(client_fd, "rigctl not available");
|
||||
return;
|
||||
}
|
||||
|
||||
cJSON* command = cJSON_GetObjectItemCaseSensitive(request, "command");
|
||||
if (!cJSON_IsString(command) || !command->valuestring) {
|
||||
send_error(client_fd, "missing 'command' field");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string result = iface_.rigctl_command(command->valuestring);
|
||||
|
||||
cJSON* response = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(response, "ok", 1);
|
||||
cJSON_AddStringToObject(response, "response", result.c_str());
|
||||
send_json(client_fd, response);
|
||||
cJSON_Delete(response);
|
||||
}
|
||||
|
||||
void handle_tx(int client_fd, cJSON* request) {
|
||||
if (!iface_.tx_data) {
|
||||
send_error(client_fd, "tx not available");
|
||||
return;
|
||||
}
|
||||
|
||||
cJSON* data_item = cJSON_GetObjectItemCaseSensitive(request, "data");
|
||||
if (!cJSON_IsString(data_item) || !data_item->valuestring) {
|
||||
send_error(client_fd, "missing 'data' field (base64)");
|
||||
return;
|
||||
}
|
||||
|
||||
auto payload = base64_decode(data_item->valuestring, strlen(data_item->valuestring));
|
||||
if (payload.empty()) {
|
||||
send_error(client_fd, "empty or invalid base64 data");
|
||||
return;
|
||||
}
|
||||
|
||||
// Per-packet oper_mode override: -1 means use default
|
||||
int oper_mode = -1;
|
||||
cJSON* mode_item = cJSON_GetObjectItemCaseSensitive(request, "oper_mode");
|
||||
if (cJSON_IsNumber(mode_item)) {
|
||||
oper_mode = mode_item->valueint;
|
||||
}
|
||||
|
||||
if (iface_.tx_data(payload, oper_mode)) {
|
||||
cJSON* response = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(response, "ok", 1);
|
||||
cJSON_AddNumberToObject(response, "size", payload.size());
|
||||
send_json(client_fd, response);
|
||||
cJSON_Delete(response);
|
||||
} else {
|
||||
send_error(client_fd, "tx failed");
|
||||
}
|
||||
}
|
||||
|
||||
void send_json(int client_fd, cJSON* json) {
|
||||
char* str = cJSON_PrintUnformatted(json);
|
||||
if (!str) return;
|
||||
|
||||
uint32_t len = strlen(str);
|
||||
uint8_t header[4];
|
||||
header[0] = (len >> 24) & 0xFF;
|
||||
header[1] = (len >> 16) & 0xFF;
|
||||
header[2] = (len >> 8) & 0xFF;
|
||||
header[3] = len & 0xFF;
|
||||
|
||||
// Send header + body (best effort, non-blocking)
|
||||
::send(client_fd, header, 4, MSG_NOSIGNAL);
|
||||
::send(client_fd, str, len, MSG_NOSIGNAL);
|
||||
|
||||
cJSON_free(str);
|
||||
}
|
||||
|
||||
void send_error(int client_fd, const char* error) {
|
||||
cJSON* response = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(response, "ok", 0);
|
||||
cJSON_AddStringToObject(response, "error", error);
|
||||
send_json(client_fd, response);
|
||||
cJSON_Delete(response);
|
||||
}
|
||||
|
||||
int port_;
|
||||
std::string bind_address_;
|
||||
TNCInterface iface_;
|
||||
int server_fd_ = -1;
|
||||
std::atomic<bool> running_{false};
|
||||
std::thread thread_;
|
||||
std::recursive_mutex clients_mutex_;
|
||||
std::list<Client> clients_;
|
||||
};
|
||||
314
kiss_tnc.cc
314
kiss_tnc.cc
|
|
@ -34,6 +34,7 @@
|
|||
#include "cm108_ptt.hh"
|
||||
#endif
|
||||
#include "modem.hh"
|
||||
#include "control_port.hh"
|
||||
|
||||
#ifdef WITH_UI
|
||||
#include "tnc_ui.hh"
|
||||
|
|
@ -386,7 +387,7 @@ private:
|
|||
if (g_verbose) {
|
||||
std::cerr << packet_visualize(frag.data(), frag.size(), true, true) << std::endl;
|
||||
}
|
||||
tx_queue_.push(std::move(frag));
|
||||
tx_queue_.push(TxPacket(std::move(frag)));
|
||||
}
|
||||
#ifdef WITH_UI
|
||||
if (g_ui_state) {
|
||||
|
|
@ -396,14 +397,14 @@ private:
|
|||
} else {
|
||||
std::vector<uint8_t> frame_data = data;
|
||||
if (frame_data.size() > max_payload) {
|
||||
std::cerr << "Warning: Frame too large (" << frame_data.size()
|
||||
std::cerr << "Warning: Frame too large (" << frame_data.size()
|
||||
<< " > " << max_payload << "), truncating" << std::endl;
|
||||
frame_data.resize(max_payload);
|
||||
}
|
||||
if (g_verbose) {
|
||||
std::cerr << packet_visualize(frame_data.data(), frame_data.size(), true, config_.fragmentation_enabled) << std::endl;
|
||||
}
|
||||
tx_queue_.push(frame_data);
|
||||
tx_queue_.push(TxPacket(frame_data));
|
||||
#ifdef WITH_UI
|
||||
if (g_ui_state) {
|
||||
g_ui_state->tx_queue_size = tx_queue_.size();
|
||||
|
|
@ -462,8 +463,8 @@ private:
|
|||
std::mt19937 gen(rd());
|
||||
|
||||
while (tx_running_ && g_running) {
|
||||
std::vector<uint8_t> frame;
|
||||
if (tx_queue_.pop(frame)) {
|
||||
TxPacket pkt;
|
||||
if (tx_queue_.pop(pkt)) {
|
||||
#ifdef WITH_UI
|
||||
if (g_ui_state) {
|
||||
g_ui_state->tx_queue_size = tx_queue_.size();
|
||||
|
|
@ -522,40 +523,46 @@ private:
|
|||
}
|
||||
}
|
||||
|
||||
transmit(frame);
|
||||
transmit(pkt.data, pkt.oper_mode);
|
||||
} else {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void transmit(const std::vector<uint8_t>& data) {
|
||||
ui_log("TX: " + std::to_string(data.size()) + " bytes");
|
||||
void transmit(const std::vector<uint8_t>& data, int oper_mode_override = -1) {
|
||||
int tx_mode = (oper_mode_override >= 0) ? oper_mode_override : modem_config_.oper_mode;
|
||||
|
||||
if (oper_mode_override >= 0) {
|
||||
ui_log("TX: " + std::to_string(data.size()) + " bytes (mode override)");
|
||||
} else {
|
||||
ui_log("TX: " + std::to_string(data.size()) + " bytes");
|
||||
}
|
||||
if (g_verbose) {
|
||||
std::cerr << packet_visualize(data.data(), data.size(), true, config_.fragmentation_enabled) << std::endl;
|
||||
}
|
||||
|
||||
|
||||
if (config_.tx_blanking_enabled) {
|
||||
tx_blanking_active_ = true;
|
||||
}
|
||||
|
||||
|
||||
#ifdef WITH_UI
|
||||
if (g_ui_state) {
|
||||
g_ui_state->transmitting = true;
|
||||
g_ui_state->tx_frame_count++;
|
||||
g_ui_state->add_packet(true, data.size(), 0);
|
||||
g_ui_state->add_packet(true, data.size(), 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
// Add length prefix framing
|
||||
auto framed_data = frame_with_length(data);
|
||||
|
||||
|
||||
// Encode to audio
|
||||
auto samples = encoder_->encode(
|
||||
framed_data.data(), framed_data.size(),
|
||||
modem_config_.center_freq,
|
||||
modem_config_.call_sign,
|
||||
modem_config_.oper_mode
|
||||
tx_mode
|
||||
);
|
||||
|
||||
if (samples.empty()) {
|
||||
|
|
@ -661,7 +668,7 @@ private:
|
|||
}
|
||||
|
||||
tx_blanking_active_ = false;
|
||||
|
||||
|
||||
#ifdef WITH_UI
|
||||
if (g_ui_state) {
|
||||
g_ui_state->transmitting = false;
|
||||
|
|
@ -885,9 +892,9 @@ private:
|
|||
|
||||
int server_fd_ = -1;
|
||||
std::list<std::unique_ptr<ClientConnection>> clients_;
|
||||
std::mutex clients_mutex_;
|
||||
mutable std::mutex clients_mutex_;
|
||||
|
||||
PacketQueue<std::vector<uint8_t>> tx_queue_;
|
||||
PacketQueue<TxPacket> tx_queue_;
|
||||
std::atomic<bool> tx_running_{false};
|
||||
std::atomic<bool> rx_running_{false};
|
||||
|
||||
|
|
@ -895,7 +902,7 @@ private:
|
|||
Reassembler reassembler_;
|
||||
|
||||
// TX lockout - prevents TX while receiving
|
||||
std::mutex lockout_mutex_;
|
||||
mutable std::mutex lockout_mutex_;
|
||||
std::chrono::steady_clock::time_point tx_lockout_until_;
|
||||
static constexpr float RX_LOCKOUT_SECONDS = 0.5f;
|
||||
|
||||
|
|
@ -955,7 +962,43 @@ public:
|
|||
}
|
||||
|
||||
TNCConfig& get_config() { return config_; }
|
||||
|
||||
|
||||
int get_payload_size() const { return payload_size_; }
|
||||
|
||||
struct DecoderStats {
|
||||
int sync_count, preamble_errors, symbol_errors, crc_errors;
|
||||
float last_snr, last_ber, ber_ema;
|
||||
};
|
||||
|
||||
DecoderStats get_decoder_stats() const {
|
||||
return {
|
||||
decoder_->stats_sync_count,
|
||||
decoder_->stats_preamble_errors,
|
||||
decoder_->stats_symbol_errors,
|
||||
decoder_->stats_crc_errors,
|
||||
decoder_->get_last_snr(),
|
||||
decoder_->get_last_ber(),
|
||||
decoder_->get_ber_ema()
|
||||
};
|
||||
}
|
||||
|
||||
bool is_transmitting() const { return tx_blanking_active_.load(); }
|
||||
|
||||
bool is_receiving() const {
|
||||
std::lock_guard<std::mutex> lock(lockout_mutex_);
|
||||
return std::chrono::steady_clock::now() < tx_lockout_until_;
|
||||
}
|
||||
|
||||
int get_client_count() const {
|
||||
std::lock_guard<std::mutex> lock(clients_mutex_);
|
||||
return clients_.size();
|
||||
}
|
||||
|
||||
std::string rigctl_command(const std::string& cmd) {
|
||||
if (rigctl_) return rigctl_->send_command(cmd);
|
||||
return "ERR: rigctl not enabled";
|
||||
}
|
||||
|
||||
bool is_rigctl_connected() const {
|
||||
if (rigctl_) return rigctl_->is_connected();
|
||||
return false;
|
||||
|
|
@ -974,17 +1017,27 @@ public:
|
|||
}
|
||||
|
||||
void queue_data(const std::vector<uint8_t>& data) {
|
||||
size_t max_payload = payload_size_ - 2;
|
||||
|
||||
if (config_.fragmentation_enabled && fragmenter_.needs_fragmentation(data.size(), max_payload)) {
|
||||
auto fragments = fragmenter_.fragment(data, max_payload);
|
||||
ui_log("TX: Fragmenting " + std::to_string(data.size()) + " bytes into " +
|
||||
queue_data_ex(data, -1);
|
||||
}
|
||||
|
||||
// Queue data with an optional per-packet oper_mode override (-1 = default)
|
||||
void queue_data_ex(const std::vector<uint8_t>& data, int oper_mode) {
|
||||
size_t effective_payload;
|
||||
if (oper_mode >= 0) {
|
||||
effective_payload = encoder_->get_payload_size(oper_mode) - 2;
|
||||
} else {
|
||||
effective_payload = payload_size_ - 2;
|
||||
}
|
||||
|
||||
if (config_.fragmentation_enabled && fragmenter_.needs_fragmentation(data.size(), effective_payload)) {
|
||||
auto fragments = fragmenter_.fragment(data, effective_payload);
|
||||
ui_log("TX: Fragmenting " + std::to_string(data.size()) + " bytes into " +
|
||||
std::to_string(fragments.size()) + " fragments");
|
||||
for (auto& frag : fragments) {
|
||||
tx_queue_.push(std::move(frag));
|
||||
tx_queue_.push(TxPacket(std::move(frag), oper_mode));
|
||||
}
|
||||
} else {
|
||||
tx_queue_.push(data);
|
||||
tx_queue_.push(TxPacket(data, oper_mode));
|
||||
}
|
||||
#ifdef WITH_UI
|
||||
if (g_ui_state) {
|
||||
|
|
@ -992,13 +1045,23 @@ public:
|
|||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Compute oper_mode for a given short_frame setting using current modulation/code_rate
|
||||
int compute_oper_mode(bool short_frame) const {
|
||||
return ModemConfig::encode_mode(
|
||||
config_.modulation.c_str(),
|
||||
config_.code_rate.c_str(),
|
||||
short_frame
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
void print_help(const char* prog) {
|
||||
std::cerr << "MODEM73\n\n"
|
||||
<< "Usage: " << prog << " [options]\n\n"
|
||||
<< "Options:\n"
|
||||
<< " -p, --port PORT TCP port (default: 8001)\n"
|
||||
<< " -p, --port PORT KISS TCP port (default: 8001)\n"
|
||||
<< " --control-port PORT Control port (default: 8073, 0 to disable)\n"
|
||||
<< " -d, --device DEV Audio device for both I/O\n"
|
||||
<< " --input-device DEV Audio input device\n"
|
||||
<< " --output-device DEV Audio output device\n"
|
||||
|
|
@ -1048,7 +1111,13 @@ void print_help(const char* prog) {
|
|||
|
||||
int main(int argc, char** argv) {
|
||||
TNCConfig config;
|
||||
|
||||
|
||||
// Track which settings were explicitly set on CLI
|
||||
bool cli_port = false;
|
||||
bool cli_control_port = false;
|
||||
bool cli_callsign = false;
|
||||
bool cli_ptt = false;
|
||||
|
||||
// Parse arguments
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
std::string arg = argv[i];
|
||||
|
|
@ -1076,6 +1145,10 @@ int main(int argc, char** argv) {
|
|||
#endif
|
||||
} else if ((arg == "-p" || arg == "--port") && i + 1 < argc) {
|
||||
config.port = std::atoi(argv[++i]);
|
||||
cli_port = true;
|
||||
} else if (arg == "--control-port" && i + 1 < argc) {
|
||||
config.control_port = std::atoi(argv[++i]);
|
||||
cli_control_port = true;
|
||||
} else if ((arg == "-d" || arg == "--device") && i + 1 < argc) {
|
||||
// Set both input and output to same device
|
||||
config.audio_input_device = argv[++i];
|
||||
|
|
@ -1086,6 +1159,7 @@ int main(int argc, char** argv) {
|
|||
config.audio_output_device = argv[++i];
|
||||
} else if ((arg == "-c" || arg == "--callsign") && i + 1 < argc) {
|
||||
config.callsign = argv[++i];
|
||||
cli_callsign = true;
|
||||
} else if ((arg == "-m" || arg == "--modulation") && i + 1 < argc) {
|
||||
config.modulation = argv[++i];
|
||||
} else if ((arg == "-r" || arg == "--rate") && i + 1 < argc) {
|
||||
|
|
@ -1118,6 +1192,7 @@ int main(int argc, char** argv) {
|
|||
return 1;
|
||||
}
|
||||
} else if (arg == "--ptt" && i + 1 < argc) {
|
||||
cli_ptt = true;
|
||||
std::string ptt_type = argv[++i];
|
||||
if (ptt_type == "none") config.ptt_type = PTTType::NONE;
|
||||
else if (ptt_type == "rigctl") config.ptt_type = PTTType::RIGCTL;
|
||||
|
|
@ -1213,8 +1288,9 @@ int main(int argc, char** argv) {
|
|||
|
||||
// Try to load saved settings
|
||||
if (ui_state.load_settings()) {
|
||||
// Apply loaded settings to config
|
||||
config.callsign = ui_state.callsign;
|
||||
// Apply loaded settings to config
|
||||
if (!cli_callsign)
|
||||
config.callsign = ui_state.callsign;
|
||||
config.center_freq = ui_state.center_freq;
|
||||
config.modulation = MODULATION_OPTIONS[ui_state.modulation_index];
|
||||
config.code_rate = CODE_RATE_OPTIONS[ui_state.code_rate_index];
|
||||
|
|
@ -1229,7 +1305,8 @@ int main(int argc, char** argv) {
|
|||
config.audio_input_device = ui_state.audio_input_device;
|
||||
config.audio_output_device = ui_state.audio_output_device;
|
||||
// PTT settings
|
||||
config.ptt_type = static_cast<PTTType>(ui_state.ptt_type_index);
|
||||
if (!cli_ptt)
|
||||
config.ptt_type = static_cast<PTTType>(ui_state.ptt_type_index);
|
||||
config.vox_tone_freq = ui_state.vox_tone_freq;
|
||||
config.vox_lead_ms = ui_state.vox_lead_ms;
|
||||
config.vox_tail_ms = ui_state.vox_tail_ms;
|
||||
|
|
@ -1241,9 +1318,10 @@ int main(int argc, char** argv) {
|
|||
config.com_invert_rts = ui_state.com_invert_rts;
|
||||
|
||||
|
||||
// Network settings
|
||||
config.port = ui_state.port;
|
||||
|
||||
// Network settings
|
||||
if (!cli_port)
|
||||
config.port = ui_state.port;
|
||||
|
||||
// Find audio device indices
|
||||
for (size_t i = 0; i < ui_state.available_input_devices.size(); i++) {
|
||||
if (ui_state.available_input_devices[i] == ui_state.audio_input_device) {
|
||||
|
|
@ -1368,12 +1446,168 @@ int main(int argc, char** argv) {
|
|||
}
|
||||
}
|
||||
|
||||
while (config.control_port > 0 && !check_port_available(config.bind_address, config.control_port)) {
|
||||
std::cerr << "Error: Control port " << config.control_port << " is already in use" << std::endl;
|
||||
|
||||
if (!g_use_ui) {
|
||||
std::cerr << "Use --control-port to specify a different port." << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::cerr << "\nEnter a different control port (or 'q' to quit, 0 to disable): ";
|
||||
std::string input;
|
||||
if (!std::getline(std::cin, input) || input.empty() || input == "q" || input == "Q") {
|
||||
std::cerr << "Exiting." << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
int new_port = std::stoi(input);
|
||||
if (new_port < 0 || new_port > 65535) {
|
||||
std::cerr << "Invalid port number. Must be 0-65535." << std::endl;
|
||||
continue;
|
||||
}
|
||||
config.control_port = new_port;
|
||||
if (new_port == 0)
|
||||
std::cerr << "Control port disabled." << std::endl;
|
||||
else
|
||||
std::cerr << "Trying control port " << config.control_port << "..." << std::endl;
|
||||
} catch (const std::exception&) {
|
||||
std::cerr << "Invalid input. Please enter a number." << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
KISSTNC tnc(config);
|
||||
|
||||
|
||||
// Set up control port
|
||||
std::unique_ptr<ControlPort> ctrl;
|
||||
if (config.control_port > 0) {
|
||||
ControlPort::TNCInterface ctrl_iface;
|
||||
|
||||
ctrl_iface.get_status = [&tnc]() -> cJSON* {
|
||||
cJSON* j = cJSON_CreateObject();
|
||||
auto stats = tnc.get_decoder_stats();
|
||||
|
||||
// Channel state
|
||||
const char* state = "idle";
|
||||
if (tnc.is_transmitting()) state = "tx";
|
||||
else if (tnc.is_receiving()) state = "rx";
|
||||
cJSON_AddStringToObject(j, "channel_state", state);
|
||||
|
||||
cJSON_AddBoolToObject(j, "ptt_on", tnc.is_transmitting());
|
||||
cJSON_AddNumberToObject(j, "rx_frame_count", stats.sync_count - stats.preamble_errors - stats.crc_errors);
|
||||
cJSON_AddNumberToObject(j, "tx_frame_count", 0); // TODO: add tx counter to KISSTNC
|
||||
cJSON_AddNumberToObject(j, "rx_error_count", stats.preamble_errors + stats.crc_errors);
|
||||
cJSON_AddNumberToObject(j, "sync_count", stats.sync_count);
|
||||
cJSON_AddNumberToObject(j, "preamble_errors", stats.preamble_errors);
|
||||
cJSON_AddNumberToObject(j, "symbol_errors", stats.symbol_errors);
|
||||
cJSON_AddNumberToObject(j, "crc_errors", stats.crc_errors);
|
||||
cJSON_AddNumberToObject(j, "last_snr", stats.last_snr);
|
||||
cJSON_AddNumberToObject(j, "last_ber", stats.last_ber);
|
||||
cJSON_AddNumberToObject(j, "ber_ema", stats.ber_ema);
|
||||
cJSON_AddNumberToObject(j, "client_count", tnc.get_client_count());
|
||||
cJSON_AddBoolToObject(j, "rigctl_connected", tnc.is_rigctl_connected());
|
||||
cJSON_AddBoolToObject(j, "audio_connected", tnc.is_audio_healthy());
|
||||
|
||||
return j;
|
||||
};
|
||||
|
||||
ctrl_iface.get_config = [&tnc]() -> cJSON* {
|
||||
cJSON* j = cJSON_CreateObject();
|
||||
auto& cfg = tnc.get_config();
|
||||
|
||||
cJSON_AddStringToObject(j, "callsign", cfg.callsign.c_str());
|
||||
cJSON_AddStringToObject(j, "modulation", cfg.modulation.c_str());
|
||||
cJSON_AddStringToObject(j, "code_rate", cfg.code_rate.c_str());
|
||||
cJSON_AddBoolToObject(j, "short_frame", cfg.short_frame);
|
||||
cJSON_AddNumberToObject(j, "center_freq", cfg.center_freq);
|
||||
cJSON_AddNumberToObject(j, "payload_size", tnc.get_payload_size());
|
||||
cJSON_AddBoolToObject(j, "csma_enabled", cfg.csma_enabled);
|
||||
cJSON_AddNumberToObject(j, "carrier_threshold_db", cfg.carrier_threshold_db);
|
||||
cJSON_AddNumberToObject(j, "p_persistence", cfg.p_persistence);
|
||||
cJSON_AddNumberToObject(j, "slot_time_ms", cfg.slot_time_ms);
|
||||
cJSON_AddBoolToObject(j, "tx_blanking_enabled", cfg.tx_blanking_enabled);
|
||||
|
||||
return j;
|
||||
};
|
||||
|
||||
ctrl_iface.set_config = [&tnc](cJSON* params) -> bool {
|
||||
TNCConfig new_config = tnc.get_config();
|
||||
|
||||
cJSON* item;
|
||||
if ((item = cJSON_GetObjectItemCaseSensitive(params, "callsign")) && cJSON_IsString(item))
|
||||
new_config.callsign = item->valuestring;
|
||||
if ((item = cJSON_GetObjectItemCaseSensitive(params, "modulation")) && cJSON_IsString(item))
|
||||
new_config.modulation = item->valuestring;
|
||||
if ((item = cJSON_GetObjectItemCaseSensitive(params, "code_rate")) && cJSON_IsString(item))
|
||||
new_config.code_rate = item->valuestring;
|
||||
if ((item = cJSON_GetObjectItemCaseSensitive(params, "short_frame")) && cJSON_IsBool(item))
|
||||
new_config.short_frame = cJSON_IsTrue(item);
|
||||
if ((item = cJSON_GetObjectItemCaseSensitive(params, "center_freq")) && cJSON_IsNumber(item))
|
||||
new_config.center_freq = item->valueint;
|
||||
if ((item = cJSON_GetObjectItemCaseSensitive(params, "csma_enabled")) && cJSON_IsBool(item))
|
||||
new_config.csma_enabled = cJSON_IsTrue(item);
|
||||
if ((item = cJSON_GetObjectItemCaseSensitive(params, "carrier_threshold_db")) && cJSON_IsNumber(item))
|
||||
new_config.carrier_threshold_db = (float)item->valuedouble;
|
||||
if ((item = cJSON_GetObjectItemCaseSensitive(params, "p_persistence")) && cJSON_IsNumber(item))
|
||||
new_config.p_persistence = item->valueint;
|
||||
if ((item = cJSON_GetObjectItemCaseSensitive(params, "slot_time_ms")) && cJSON_IsNumber(item))
|
||||
new_config.slot_time_ms = item->valueint;
|
||||
if ((item = cJSON_GetObjectItemCaseSensitive(params, "tx_blanking_enabled")) && cJSON_IsBool(item))
|
||||
new_config.tx_blanking_enabled = cJSON_IsTrue(item);
|
||||
|
||||
tnc.update_config(new_config);
|
||||
|
||||
#ifdef WITH_UI
|
||||
// Sync config back to TUI state so the UI reflects changes
|
||||
if (g_ui_state) {
|
||||
g_ui_state->callsign = new_config.callsign;
|
||||
g_ui_state->center_freq = new_config.center_freq;
|
||||
g_ui_state->short_frame = new_config.short_frame;
|
||||
g_ui_state->csma_enabled = new_config.csma_enabled;
|
||||
g_ui_state->carrier_threshold_db = new_config.carrier_threshold_db;
|
||||
g_ui_state->p_persistence = new_config.p_persistence;
|
||||
g_ui_state->slot_time_ms = new_config.slot_time_ms;
|
||||
g_ui_state->tx_blanking_enabled = new_config.tx_blanking_enabled;
|
||||
|
||||
// Map modulation string back to index
|
||||
for (size_t i = 0; i < MODULATION_OPTIONS.size(); i++) {
|
||||
if (MODULATION_OPTIONS[i] == new_config.modulation) {
|
||||
g_ui_state->modulation_index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Map code rate string back to index
|
||||
for (size_t i = 0; i < CODE_RATE_OPTIONS.size(); i++) {
|
||||
if (CODE_RATE_OPTIONS[i] == new_config.code_rate) {
|
||||
g_ui_state->code_rate_index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
g_ui_state->update_modem_info();
|
||||
}
|
||||
#endif
|
||||
return true;
|
||||
};
|
||||
|
||||
ctrl_iface.rigctl_command = [&tnc](const std::string& cmd) -> std::string {
|
||||
return tnc.rigctl_command(cmd);
|
||||
};
|
||||
|
||||
ctrl_iface.tx_data = [&tnc](const std::vector<uint8_t>& data, int oper_mode) -> bool {
|
||||
tnc.queue_data_ex(data, oper_mode);
|
||||
return true;
|
||||
};
|
||||
|
||||
ctrl = std::make_unique<ControlPort>(config.control_port, config.bind_address, ctrl_iface);
|
||||
ctrl->start();
|
||||
}
|
||||
|
||||
#ifdef WITH_UI
|
||||
if (g_use_ui) {
|
||||
ui_state.on_settings_changed = [&tnc](TNCUIState& state) {
|
||||
ui_state.on_settings_changed = [&tnc, &ctrl](TNCUIState& state) {
|
||||
TNCConfig new_config = tnc.get_config();
|
||||
new_config.callsign = state.callsign;
|
||||
new_config.center_freq = state.center_freq;
|
||||
|
|
@ -1393,13 +1627,14 @@ int main(int argc, char** argv) {
|
|||
new_config.vox_tone_freq = state.vox_tone_freq;
|
||||
new_config.vox_lead_ms = state.vox_lead_ms;
|
||||
new_config.vox_tail_ms = state.vox_tail_ms;
|
||||
// COM PTT settings
|
||||
// COM PTT settings
|
||||
new_config.com_port = state.com_port;
|
||||
new_config.com_ptt_line = state.com_ptt_line;
|
||||
new_config.com_invert_dtr = state.com_invert_dtr;
|
||||
new_config.com_invert_rts = state.com_invert_rts;
|
||||
|
||||
|
||||
tnc.update_config(new_config);
|
||||
if (ctrl) ctrl->notify_config_changed();
|
||||
};
|
||||
|
||||
// Set up send data callback for UTILS tab
|
||||
|
|
@ -1434,13 +1669,14 @@ int main(int argc, char** argv) {
|
|||
status_thread.join();
|
||||
tnc_thread.join();
|
||||
|
||||
|
||||
|
||||
} else {
|
||||
tnc.run();
|
||||
}
|
||||
#else
|
||||
tnc.run();
|
||||
#endif
|
||||
if (ctrl) ctrl->stop();
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "error " << e.what() << std::endl;
|
||||
return 1;
|
||||
|
|
|
|||
11
kiss_tnc.hh
11
kiss_tnc.hh
|
|
@ -104,6 +104,9 @@ struct TNCConfig {
|
|||
// TX blanking
|
||||
bool tx_blanking_enabled = false;
|
||||
|
||||
// Control port
|
||||
int control_port = 8073;
|
||||
|
||||
// Settings file path
|
||||
std::string config_file = "";
|
||||
};
|
||||
|
|
@ -378,6 +381,14 @@ inline std::vector<uint8_t> unframe_length(const uint8_t* data, size_t total_len
|
|||
return std::vector<uint8_t>(data + 2, data + 2 + payload_len);
|
||||
}
|
||||
|
||||
// TX packet with optional per-packet mode override
|
||||
struct TxPacket {
|
||||
std::vector<uint8_t> data;
|
||||
int oper_mode; // -1 = use default mode
|
||||
TxPacket() : oper_mode(-1) {}
|
||||
TxPacket(std::vector<uint8_t> d, int mode = -1) : data(std::move(d)), oper_mode(mode) {}
|
||||
};
|
||||
|
||||
namespace Frag {
|
||||
constexpr uint8_t MAGIC = 0xF3;
|
||||
constexpr size_t HEADER_SIZE = 5;
|
||||
|
|
|
|||
|
|
@ -342,22 +342,15 @@ public:
|
|||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
float measure_level(int duration_ms = 100) {
|
||||
(void)duration_ms;
|
||||
if (!capture_open_) return -100.0f;
|
||||
|
||||
int frames = (sample_rate_ * duration_ms) / 1000;
|
||||
std::vector<float> buffer(frames);
|
||||
|
||||
int total_read = read(buffer.data(), frames);
|
||||
if (total_read <= 0) return -100.0f;
|
||||
|
||||
float sum_sq = 0.0f;
|
||||
for (int i = 0; i < total_read; i++) {
|
||||
sum_sq += buffer[i] * buffer[i];
|
||||
}
|
||||
float rms = std::sqrt(sum_sq / total_read);
|
||||
|
||||
float rms = std::sqrt(capture_level_sum_.load() /
|
||||
std::max(1.0f, (float)capture_level_count_.load()));
|
||||
if (rms < 1e-10f) return -100.0f;
|
||||
return 20.0f * std::log10(rms);
|
||||
}
|
||||
|
|
@ -411,11 +404,17 @@ private:
|
|||
|
||||
ma_uint32 to_write = std::min((ma_uint32)available, frame_count);
|
||||
|
||||
float sum_sq = 0.0f;
|
||||
for (ma_uint32 i = 0; i < to_write; i++) {
|
||||
self->capture_buffer_[(write_pos + i) % RING_BUFFER_SIZE] = in[i];
|
||||
sum_sq += in[i] * in[i];
|
||||
}
|
||||
|
||||
|
||||
self->capture_write_pos_ = (write_pos + to_write) % RING_BUFFER_SIZE;
|
||||
|
||||
// Update running level for CSMA exponential moving average
|
||||
self->capture_level_sum_ = sum_sq;
|
||||
self->capture_level_count_ = to_write;
|
||||
}
|
||||
|
||||
std::string capture_device_id_;
|
||||
|
|
@ -441,4 +440,8 @@ private:
|
|||
|
||||
int consecutive_read_failures_ = 0;
|
||||
int consecutive_write_failures_ = 0;
|
||||
|
||||
// Running signal level for CSMA
|
||||
std::atomic<float> capture_level_sum_{0.0f};
|
||||
std::atomic<int> capture_level_count_{1};
|
||||
};
|
||||
|
|
|
|||
6
modem.hh
6
modem.hh
|
|
@ -445,6 +445,7 @@ public:
|
|||
samples_needed_ = 0;
|
||||
k_ = 0;
|
||||
}
|
||||
|
||||
|
||||
// Get average SNR from last successful decode
|
||||
value get_last_snr() const { return last_avg_snr_; }
|
||||
|
|
@ -664,6 +665,7 @@ private:
|
|||
samples_needed_ = symbol_pos + symbol_len + 2 * extended_len;
|
||||
} else {
|
||||
++stats_preamble_errors;
|
||||
reset();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
|
@ -678,7 +680,7 @@ private:
|
|||
if (!process_symbol(symbol_index_)) {
|
||||
// Error, go back to searching
|
||||
++stats_symbol_errors;
|
||||
state_ = State::SEARCHING;
|
||||
reset();
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -687,7 +689,7 @@ private:
|
|||
if (symbol_index_ > symbol_count) {
|
||||
// All symbols collected
|
||||
decode_frame(callback);
|
||||
state_ = State::SEARCHING;
|
||||
reset();
|
||||
} else {
|
||||
samples_needed_ = extended_len;
|
||||
}
|
||||
|
|
|
|||
136
rigctl_ptt.hh
136
rigctl_ptt.hh
|
|
@ -3,11 +3,13 @@
|
|||
#include <string>
|
||||
#include <iostream>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <unistd.h>
|
||||
#include <netdb.h>
|
||||
#include <poll.h>
|
||||
|
||||
class RigctlPTT {
|
||||
public:
|
||||
|
|
@ -19,69 +21,32 @@ public:
|
|||
}
|
||||
|
||||
bool connect() {
|
||||
if (connected_) return true;
|
||||
|
||||
sock_ = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (sock_ < 0) {
|
||||
std::cerr << "rigctl: Failed to create socket" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
struct hostent* server = gethostbyname(host_.c_str());
|
||||
if (!server) {
|
||||
std::cerr << "rigctl PTT: Can't connect to host " << host_ << std::endl;
|
||||
close(sock_);
|
||||
sock_ = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
struct sockaddr_in addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
memcpy(&addr.sin_addr.s_addr, server->h_addr, server->h_length);
|
||||
addr.sin_port = htons(port_);
|
||||
|
||||
if (::connect(sock_, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
|
||||
std::cerr << "rigctl: Can't connect to " << host_ << ":" << port_ << std::endl;
|
||||
close(sock_);
|
||||
sock_ = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
connected_ = true;
|
||||
std::cerr << "rigctl: Connected to " << host_ << ":" << port_ << std::endl;
|
||||
return true;
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return connect_locked();
|
||||
}
|
||||
|
||||
|
||||
|
||||
void disconnect() {
|
||||
if (sock_ >= 0) {
|
||||
if (ptt_on_) {
|
||||
set_ptt(false);
|
||||
}
|
||||
close(sock_);
|
||||
sock_ = -1;
|
||||
}
|
||||
connected_ = false;
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
disconnect_locked();
|
||||
}
|
||||
|
||||
|
||||
|
||||
bool set_ptt(bool on) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!connected_) {
|
||||
if (!connect()) return false;
|
||||
if (!connect_locked()) return false;
|
||||
}
|
||||
|
||||
|
||||
// T 1 (PTT on) or T 0 (PTT off)
|
||||
std::string cmd = on ? "T 1\n" : "T 0\n";
|
||||
|
||||
|
||||
if (send(sock_, cmd.c_str(), cmd.length(), 0) < 0) {
|
||||
std::cerr << "rigctl: Failed to send PTT command" << std::endl;
|
||||
disconnect();
|
||||
disconnect_locked();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// read response
|
||||
char response[256];
|
||||
int n = recv(sock_, response, sizeof(response) - 1, 0);
|
||||
|
|
@ -102,17 +67,94 @@ public:
|
|||
return true;
|
||||
}
|
||||
|
||||
// Send an arbitrary rigctld command and return the response
|
||||
std::string send_command(const std::string& cmd) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!connected_) {
|
||||
if (!connect_locked()) return "ERR: not connected";
|
||||
}
|
||||
|
||||
std::string wire = cmd + "\n";
|
||||
if (::send(sock_, wire.c_str(), wire.length(), 0) < 0) {
|
||||
disconnect_locked();
|
||||
return "ERR: send failed";
|
||||
}
|
||||
|
||||
// Read response with timeout
|
||||
std::string result;
|
||||
char buf[1024];
|
||||
struct pollfd pfd = {sock_, POLLIN, 0};
|
||||
|
||||
while (true) {
|
||||
int ready = poll(&pfd, 1, 500);
|
||||
if (ready <= 0) break;
|
||||
int n = recv(sock_, buf, sizeof(buf) - 1, 0);
|
||||
if (n <= 0) break;
|
||||
buf[n] = '\0';
|
||||
result += buf;
|
||||
// If we got RPRT, that's the end of the response
|
||||
if (result.find("RPRT") != std::string::npos) break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
bool ptt_on() const { return ptt_on_; }
|
||||
bool is_connected() const { return connected_; }
|
||||
|
||||
|
||||
private:
|
||||
bool connect_locked() {
|
||||
if (connected_) return true;
|
||||
|
||||
sock_ = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (sock_ < 0) {
|
||||
std::cerr << "rigctl: Failed to create socket" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
struct hostent* server = gethostbyname(host_.c_str());
|
||||
if (!server) {
|
||||
std::cerr << "rigctl PTT: Can't connect to host " << host_ << std::endl;
|
||||
close(sock_);
|
||||
sock_ = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
struct sockaddr_in addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
memcpy(&addr.sin_addr.s_addr, server->h_addr, server->h_length);
|
||||
addr.sin_port = htons(port_);
|
||||
|
||||
if (::connect(sock_, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
|
||||
std::cerr << "rigctl: Can't connect to " << host_ << ":" << port_ << std::endl;
|
||||
close(sock_);
|
||||
sock_ = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
connected_ = true;
|
||||
std::cerr << "rigctl: Connected to " << host_ << ":" << port_ << std::endl;
|
||||
return true;
|
||||
}
|
||||
|
||||
void disconnect_locked() {
|
||||
if (sock_ >= 0) {
|
||||
close(sock_);
|
||||
sock_ = -1;
|
||||
}
|
||||
connected_ = false;
|
||||
ptt_on_ = false;
|
||||
}
|
||||
|
||||
std::string host_;
|
||||
int port_;
|
||||
int sock_ = -1;
|
||||
bool connected_ = false;
|
||||
bool ptt_on_ = false;
|
||||
std::mutex mutex_;
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue