kopia lustrzana https://github.com/AlexandreRouma/SDRPlusPlus
new scripting system
rodzic
ac068036b8
commit
46d5b8a750
|
@ -14,6 +14,9 @@
|
|||
#include <stb_image.h>
|
||||
#include <config.h>
|
||||
#include <core.h>
|
||||
#include <duktape/duktape.h>
|
||||
#include <duktape/duk_console.h>
|
||||
#include <scripting.h>
|
||||
|
||||
#include <dsp/block.h>
|
||||
|
||||
|
@ -26,6 +29,7 @@
|
|||
|
||||
namespace core {
|
||||
ConfigManager configManager;
|
||||
ScriptManager scriptManager;
|
||||
|
||||
void setInputSampleRate(double samplerate) {
|
||||
// NOTE: Zoom controls won't work
|
||||
|
@ -50,12 +54,55 @@ static void maximized_callback(GLFWwindow* window, int n) {
|
|||
}
|
||||
}
|
||||
|
||||
duk_ret_t test_func(duk_context *ctx) {
|
||||
printf("Hello from C++\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// main
|
||||
int sdrpp_main() {
|
||||
#ifdef _WIN32
|
||||
//FreeConsole();
|
||||
#endif
|
||||
|
||||
|
||||
// TESTING
|
||||
|
||||
|
||||
// duk_context* ctx = duk_create_heap_default();
|
||||
// duk_console_init(ctx, DUK_CONSOLE_PROXY_WRAPPER);
|
||||
|
||||
// std::ifstream file("test.js", std::ios::in);
|
||||
// std::stringstream ss;
|
||||
// ss << file.rdbuf();
|
||||
// std::string code = ss.str();
|
||||
|
||||
// duk_idx_t baseObj = duk_push_object(ctx);
|
||||
|
||||
// duk_push_int(ctx, 42);
|
||||
// duk_put_prop_string(ctx, baseObj, "my_property");
|
||||
|
||||
// duk_push_c_function(ctx, test_func, 0);
|
||||
// duk_put_prop_string(ctx, baseObj, "my_func");
|
||||
|
||||
// duk_put_global_string(ctx, "my_object");
|
||||
// duk_push_object(ctx);
|
||||
|
||||
|
||||
|
||||
// if (duk_peval_string(ctx, code.c_str()) != 0) {
|
||||
// printf("Error: %s\n", duk_safe_to_string(ctx, -1));
|
||||
// return -1;
|
||||
// }
|
||||
// duk_pop(ctx);
|
||||
|
||||
core::scriptManager.createScript("TestScript 1", "test.js");
|
||||
core::scriptManager.createScript("TestScript 2", "test.js");
|
||||
core::scriptManager.createScript("TestScript 3", "test.js");
|
||||
core::scriptManager.createScript("TestScript 4", "test.js");
|
||||
|
||||
// TESTING
|
||||
|
||||
spdlog::info("SDR++ v" VERSION_STR);
|
||||
|
||||
// Load config
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
#pragma once
|
||||
#include <module.h>
|
||||
#include <config.h>
|
||||
#include <scripting.h>
|
||||
|
||||
namespace core {
|
||||
SDRPP_EXPORT ConfigManager configManager;
|
||||
SDRPP_EXPORT ScriptManager scriptManager;
|
||||
|
||||
void setInputSampleRate(double samplerate);
|
||||
};
|
||||
|
|
Plik diff jest za duży
Load Diff
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
* Minimal 'console' binding.
|
||||
*
|
||||
* https://github.com/DeveloperToolsWG/console-object/blob/master/api.md
|
||||
* https://developers.google.com/web/tools/chrome-devtools/debug/console/console-reference
|
||||
* https://developer.mozilla.org/en/docs/Web/API/console
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdarg.h>
|
||||
#include "duktape.h"
|
||||
#include "duk_console.h"
|
||||
|
||||
/* XXX: Add some form of log level filtering. */
|
||||
|
||||
/* XXX: Should all output be written via e.g. console.write(formattedMsg)?
|
||||
* This would make it easier for user code to redirect all console output
|
||||
* to a custom backend.
|
||||
*/
|
||||
|
||||
/* XXX: Init console object using duk_def_prop() when that call is available. */
|
||||
|
||||
static duk_ret_t duk__console_log_helper(duk_context *ctx, const char *error_name) {
|
||||
duk_uint_t flags = (duk_uint_t) duk_get_current_magic(ctx);
|
||||
FILE *output = (flags & DUK_CONSOLE_STDOUT_ONLY) ? stdout : stderr;
|
||||
duk_idx_t n = duk_get_top(ctx);
|
||||
duk_idx_t i;
|
||||
|
||||
duk_get_global_string(ctx, "console");
|
||||
duk_get_prop_string(ctx, -1, "format");
|
||||
|
||||
for (i = 0; i < n; i++) {
|
||||
if (duk_check_type_mask(ctx, i, DUK_TYPE_MASK_OBJECT)) {
|
||||
/* Slow path formatting. */
|
||||
duk_dup(ctx, -1); /* console.format */
|
||||
duk_dup(ctx, i);
|
||||
duk_call(ctx, 1);
|
||||
duk_replace(ctx, i); /* arg[i] = console.format(arg[i]); */
|
||||
}
|
||||
}
|
||||
|
||||
duk_pop_2(ctx);
|
||||
|
||||
duk_push_string(ctx, " ");
|
||||
duk_insert(ctx, 0);
|
||||
duk_join(ctx, n);
|
||||
|
||||
if (error_name) {
|
||||
duk_push_error_object(ctx, DUK_ERR_ERROR, "%s", duk_require_string(ctx, -1));
|
||||
duk_push_string(ctx, "name");
|
||||
duk_push_string(ctx, error_name);
|
||||
duk_def_prop(ctx, -3, DUK_DEFPROP_FORCE | DUK_DEFPROP_HAVE_VALUE); /* to get e.g. 'Trace: 1 2 3' */
|
||||
duk_get_prop_string(ctx, -1, "stack");
|
||||
}
|
||||
|
||||
fprintf(output, "%s\n", duk_to_string(ctx, -1));
|
||||
if (flags & DUK_CONSOLE_FLUSH) {
|
||||
fflush(output);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static duk_ret_t duk__console_assert(duk_context *ctx) {
|
||||
if (duk_to_boolean(ctx, 0)) {
|
||||
return 0;
|
||||
}
|
||||
duk_remove(ctx, 0);
|
||||
|
||||
return duk__console_log_helper(ctx, "AssertionError");
|
||||
}
|
||||
|
||||
static duk_ret_t duk__console_log(duk_context *ctx) {
|
||||
return duk__console_log_helper(ctx, NULL);
|
||||
}
|
||||
|
||||
static duk_ret_t duk__console_trace(duk_context *ctx) {
|
||||
return duk__console_log_helper(ctx, "Trace");
|
||||
}
|
||||
|
||||
static duk_ret_t duk__console_info(duk_context *ctx) {
|
||||
return duk__console_log_helper(ctx, NULL);
|
||||
}
|
||||
|
||||
static duk_ret_t duk__console_warn(duk_context *ctx) {
|
||||
return duk__console_log_helper(ctx, NULL);
|
||||
}
|
||||
|
||||
static duk_ret_t duk__console_error(duk_context *ctx) {
|
||||
return duk__console_log_helper(ctx, "Error");
|
||||
}
|
||||
|
||||
static duk_ret_t duk__console_dir(duk_context *ctx) {
|
||||
/* For now, just share the formatting of .log() */
|
||||
return duk__console_log_helper(ctx, 0);
|
||||
}
|
||||
|
||||
static void duk__console_reg_vararg_func(duk_context *ctx, duk_c_function func, const char *name, duk_uint_t flags) {
|
||||
duk_push_c_function(ctx, func, DUK_VARARGS);
|
||||
duk_push_string(ctx, "name");
|
||||
duk_push_string(ctx, name);
|
||||
duk_def_prop(ctx, -3, DUK_DEFPROP_HAVE_VALUE | DUK_DEFPROP_FORCE); /* Improve stacktraces by displaying function name */
|
||||
duk_set_magic(ctx, -1, (duk_int_t) flags);
|
||||
duk_put_prop_string(ctx, -2, name);
|
||||
}
|
||||
|
||||
void duk_console_init(duk_context *ctx, duk_uint_t flags) {
|
||||
duk_uint_t flags_orig;
|
||||
|
||||
/* If both DUK_CONSOLE_STDOUT_ONLY and DUK_CONSOLE_STDERR_ONLY where specified,
|
||||
* just turn off DUK_CONSOLE_STDOUT_ONLY and keep DUK_CONSOLE_STDERR_ONLY.
|
||||
*/
|
||||
if ((flags & DUK_CONSOLE_STDOUT_ONLY) && (flags & DUK_CONSOLE_STDERR_ONLY)) {
|
||||
flags &= ~DUK_CONSOLE_STDOUT_ONLY;
|
||||
}
|
||||
/* Remember the (possibly corrected) flags we received. */
|
||||
flags_orig = flags;
|
||||
|
||||
duk_push_object(ctx);
|
||||
|
||||
/* Custom function to format objects; user can replace.
|
||||
* For now, try JX-formatting and if that fails, fall back
|
||||
* to ToString(v).
|
||||
*/
|
||||
duk_eval_string(ctx,
|
||||
"(function (E) {"
|
||||
"return function format(v){"
|
||||
"try{"
|
||||
"return E('jx',v);"
|
||||
"}catch(e){"
|
||||
"return String(v);" /* String() allows symbols, ToString() internal algorithm doesn't. */
|
||||
"}"
|
||||
"};"
|
||||
"})(Duktape.enc)");
|
||||
duk_put_prop_string(ctx, -2, "format");
|
||||
|
||||
flags = flags_orig;
|
||||
if (!(flags & DUK_CONSOLE_STDOUT_ONLY) && !(flags & DUK_CONSOLE_STDERR_ONLY)) {
|
||||
/* No output indicators were specified; these levels go to stdout. */
|
||||
flags |= DUK_CONSOLE_STDOUT_ONLY;
|
||||
}
|
||||
duk__console_reg_vararg_func(ctx, duk__console_assert, "assert", flags);
|
||||
duk__console_reg_vararg_func(ctx, duk__console_log, "log", flags);
|
||||
duk__console_reg_vararg_func(ctx, duk__console_log, "debug", flags); /* alias to console.log */
|
||||
duk__console_reg_vararg_func(ctx, duk__console_trace, "trace", flags);
|
||||
duk__console_reg_vararg_func(ctx, duk__console_info, "info", flags);
|
||||
|
||||
flags = flags_orig;
|
||||
if (!(flags & DUK_CONSOLE_STDOUT_ONLY) && !(flags & DUK_CONSOLE_STDERR_ONLY)) {
|
||||
/* No output indicators were specified; these levels go to stderr. */
|
||||
flags |= DUK_CONSOLE_STDERR_ONLY;
|
||||
}
|
||||
duk__console_reg_vararg_func(ctx, duk__console_warn, "warn", flags);
|
||||
duk__console_reg_vararg_func(ctx, duk__console_error, "error", flags);
|
||||
duk__console_reg_vararg_func(ctx, duk__console_error, "exception", flags); /* alias to console.error */
|
||||
duk__console_reg_vararg_func(ctx, duk__console_dir, "dir", flags);
|
||||
|
||||
duk_put_global_string(ctx, "console");
|
||||
|
||||
/* Proxy wrapping: ensures any undefined console method calls are
|
||||
* ignored silently. This was required specifically by the
|
||||
* DeveloperToolsWG proposal (and was implemented also by Firefox:
|
||||
* https://bugzilla.mozilla.org/show_bug.cgi?id=629607). This is
|
||||
* apparently no longer the preferred way of implementing console.
|
||||
* When Proxy is enabled, whitelist at least .toJSON() to avoid
|
||||
* confusing JX serialization of the console object.
|
||||
*/
|
||||
|
||||
if (flags & DUK_CONSOLE_PROXY_WRAPPER) {
|
||||
/* Tolerate failure to initialize Proxy wrapper in case
|
||||
* Proxy support is disabled.
|
||||
*/
|
||||
(void) duk_peval_string_noresult(ctx,
|
||||
"(function(){"
|
||||
"var D=function(){};"
|
||||
"var W={toJSON:true};" /* whitelisted */
|
||||
"console=new Proxy(console,{"
|
||||
"get:function(t,k){"
|
||||
"var v=t[k];"
|
||||
"return typeof v==='function'||W[k]?v:D;"
|
||||
"}"
|
||||
"});"
|
||||
"})();"
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
#if !defined(DUK_CONSOLE_H_INCLUDED)
|
||||
#define DUK_CONSOLE_H_INCLUDED
|
||||
|
||||
#include "duktape.h"
|
||||
|
||||
#if defined(__cplusplus)
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Use a proxy wrapper to make undefined methods (console.foo()) no-ops. */
|
||||
#define DUK_CONSOLE_PROXY_WRAPPER (1U << 0)
|
||||
|
||||
/* Flush output after every call. */
|
||||
#define DUK_CONSOLE_FLUSH (1U << 1)
|
||||
|
||||
/* Send output to stdout only (default is mixed stdout/stderr). */
|
||||
#define DUK_CONSOLE_STDOUT_ONLY (1U << 2)
|
||||
|
||||
/* Send output to stderr only (default is mixed stdout/stderr). */
|
||||
#define DUK_CONSOLE_STDERR_ONLY (1U << 3)
|
||||
|
||||
/* Initialize the console system */
|
||||
extern void duk_console_init(duk_context *ctx, duk_uint_t flags);
|
||||
|
||||
#if defined(__cplusplus)
|
||||
}
|
||||
#endif /* end 'extern "C"' wrapper */
|
||||
|
||||
#endif /* DUK_CONSOLE_H_INCLUDED */
|
Plik diff jest za duży
Load Diff
Plik diff jest za duży
Load Diff
|
@ -59,6 +59,7 @@ void windowInit() {
|
|||
|
||||
gui::menu.registerEntry("Source", sourecmenu::draw, NULL);
|
||||
gui::menu.registerEntry("Audio", audiomenu::draw, NULL);
|
||||
gui::menu.registerEntry("Scripting", scriptingmenu::draw, NULL);
|
||||
gui::menu.registerEntry("Band Plan", bandplanmenu::draw, NULL);
|
||||
gui::menu.registerEntry("Display", displaymenu::draw, NULL);
|
||||
|
||||
|
@ -76,6 +77,7 @@ void windowInit() {
|
|||
|
||||
sourecmenu::init();
|
||||
audiomenu::init();
|
||||
scriptingmenu::init();
|
||||
bandplanmenu::init();
|
||||
displaymenu::init();
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
#include <gui/menus/display.h>
|
||||
#include <gui/menus/bandplan.h>
|
||||
#include <gui/menus/audio.h>
|
||||
#include <gui/menus/scripting.h>
|
||||
#include <gui/dialogs/credits.h>
|
||||
#include <signal_path/source.h>
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
#include <gui/menus/scripting.h>
|
||||
#include <core.h>
|
||||
#include <gui/style.h>
|
||||
|
||||
namespace scriptingmenu {
|
||||
void init() {
|
||||
|
||||
}
|
||||
|
||||
void draw(void* ctx) {
|
||||
float menuWidth = ImGui::GetContentRegionAvailWidth();
|
||||
for (const auto& [name, script] : core::scriptManager.scripts) {
|
||||
bool running = script->running;
|
||||
if (running) { style::beginDisabled(); }
|
||||
if (ImGui::Button(name.c_str(), ImVec2(menuWidth, 0))) {
|
||||
script->run();
|
||||
}
|
||||
if (running) { style::endDisabled(); }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
namespace scriptingmenu {
|
||||
void init();
|
||||
void draw(void* ctx);
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
#include <scripting.h>
|
||||
#include <duktape/duk_console.h>
|
||||
#include <version.h>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
ScriptManager::ScriptManager() {
|
||||
|
||||
}
|
||||
|
||||
ScriptManager::Script* ScriptManager::createScript(std::string name, std::string path) {
|
||||
ScriptManager::Script* script = new ScriptManager::Script(this, name, path);
|
||||
scripts[name] = script;
|
||||
return script;
|
||||
}
|
||||
|
||||
ScriptManager::Script::Script(ScriptManager* man, std::string name, std::string path) {
|
||||
this->name = name;
|
||||
manager = man;
|
||||
std::ifstream file(path, std::ios::in);
|
||||
std::stringstream ss;
|
||||
ss << file.rdbuf();
|
||||
code = ss.str();
|
||||
}
|
||||
|
||||
void ScriptManager::Script::run() {
|
||||
ctx = ScriptManager::createContext(manager, name);
|
||||
running = true;
|
||||
if (worker.joinable()) {
|
||||
worker.join();
|
||||
}
|
||||
worker = std::thread(scriptWorker, this);
|
||||
}
|
||||
|
||||
duk_context* ScriptManager::createContext(ScriptManager* _this, std::string name) {
|
||||
duk_context* ctx = duk_create_heap_default();
|
||||
|
||||
duk_console_init(ctx, DUK_CONSOLE_PROXY_WRAPPER);
|
||||
|
||||
duk_push_string(ctx, name.c_str());
|
||||
duk_put_global_string(ctx, "SCRIPT_NAME");
|
||||
|
||||
duk_idx_t sdrppBase = duk_push_object(ctx);
|
||||
|
||||
duk_push_string(ctx, VERSION_STR);
|
||||
duk_put_prop_string(ctx, sdrppBase, "version");
|
||||
|
||||
duk_idx_t modObjId = duk_push_object(ctx);
|
||||
for (const auto& [name, handler] : _this->handlers) {
|
||||
duk_idx_t objId = duk_push_object(ctx);
|
||||
handler.handler(handler.ctx, ctx, objId);
|
||||
duk_put_prop_string(ctx, modObjId, name.c_str());
|
||||
}
|
||||
duk_put_prop_string(ctx, sdrppBase, "modules");
|
||||
|
||||
duk_put_global_string(ctx, "sdrpp");
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
// TODO: Switch to spdlog
|
||||
|
||||
void ScriptManager::Script::scriptWorker(Script* script) {
|
||||
if (duk_peval_string(script->ctx, script->code.c_str()) != 0) {
|
||||
printf("Error: %s\n", duk_safe_to_string(script->ctx, -1));
|
||||
//return;
|
||||
}
|
||||
duk_destroy_heap(script->ctx);
|
||||
script->running = false;
|
||||
}
|
||||
|
||||
void ScriptManager::bindScriptRunHandler(std::string name, ScriptManager::ScriptRunHandler_t handler) {
|
||||
// TODO: check if it exists and add a "unbind" function
|
||||
handlers[name] = handler;
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
#pragma once
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <duktape/duktape.h>
|
||||
|
||||
class ScriptManager {
|
||||
public:
|
||||
ScriptManager();
|
||||
|
||||
friend class Script;
|
||||
|
||||
class Script {
|
||||
public:
|
||||
Script(ScriptManager* man, std::string name, std::string path);
|
||||
void run();
|
||||
bool running = false;
|
||||
|
||||
private:
|
||||
static void scriptWorker(Script* _this);
|
||||
|
||||
duk_context* ctx;
|
||||
std::thread worker;
|
||||
std::string code;
|
||||
std::string name;
|
||||
|
||||
|
||||
ScriptManager* manager;
|
||||
|
||||
};
|
||||
|
||||
struct ScriptRunHandler_t {
|
||||
void (*handler)(void* ctx, duk_context* dukCtx, duk_idx_t objId);
|
||||
void* ctx;
|
||||
};
|
||||
|
||||
void bindScriptRunHandler(std::string name, ScriptManager::ScriptRunHandler_t handler);
|
||||
ScriptManager::Script* createScript(std::string name, std::string path);
|
||||
|
||||
std::map<std::string, ScriptManager::Script*> scripts;
|
||||
|
||||
private:
|
||||
static duk_context* createContext(ScriptManager* _this, std::string name);
|
||||
|
||||
std::map<std::string, ScriptManager::ScriptRunHandler_t> handlers;
|
||||
|
||||
};
|
|
@ -3,6 +3,7 @@
|
|||
#include <path.h>
|
||||
#include <watcher.h>
|
||||
#include <config.h>
|
||||
#include <core.h>
|
||||
|
||||
#define CONCAT(a, b) ((std::string(a) + b).c_str())
|
||||
#define DEEMP_LIST "50µS\00075µS\000none\000"
|
||||
|
@ -26,6 +27,11 @@ public:
|
|||
sigPath.start();
|
||||
sigPath.setDemodulator(SigPath::DEMOD_FM, bandWidth);
|
||||
gui::menu.registerEntry(name, menuHandler, this);
|
||||
|
||||
ScriptManager::ScriptRunHandler_t handler;
|
||||
handler.ctx = this;
|
||||
handler.handler = scriptCreateHandler;
|
||||
core::scriptManager.bindScriptRunHandler(name, handler);
|
||||
}
|
||||
|
||||
~RadioModule() {
|
||||
|
@ -117,6 +123,32 @@ private:
|
|||
ImGui::PopItemWidth();
|
||||
}
|
||||
|
||||
static void scriptCreateHandler(void* ctx, duk_context* dukCtx, duk_idx_t objId) {
|
||||
duk_push_string(dukCtx, "Hello from modules ;)");
|
||||
duk_put_prop_string(dukCtx, objId, "test");
|
||||
|
||||
duk_push_c_function(dukCtx, duk_setDemodulator, 1);
|
||||
duk_put_prop_string(dukCtx, objId, "setDemodulator");
|
||||
|
||||
duk_push_pointer(dukCtx, ctx);
|
||||
duk_put_prop_string(dukCtx, objId, DUK_HIDDEN_SYMBOL("radio_ctx"));
|
||||
}
|
||||
|
||||
static duk_ret_t duk_setDemodulator(duk_context* dukCtx) {
|
||||
const char* str = duk_require_string(dukCtx, -1);
|
||||
std::string modName = str;
|
||||
|
||||
if (modName == "USB") {
|
||||
_this->demod = 4;
|
||||
_this->bandWidth = 3000;
|
||||
_this->bandWidthMin = 1500;
|
||||
_this->bandWidthMax = 3000;
|
||||
_this->sigPath.setDemodulator(SigPath::DEMOD_USB, _this->bandWidth);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string name;
|
||||
int demod = 1;
|
||||
int deemp = 0;
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
"Radio",
|
||||
"Recorder",
|
||||
"Audio",
|
||||
"Scripting",
|
||||
"Band Plan",
|
||||
"Display"
|
||||
],
|
||||
|
@ -20,7 +21,7 @@
|
|||
"source": "",
|
||||
"sourceSettings": {},
|
||||
"windowSize": {
|
||||
"h": 1053,
|
||||
"w": 1920
|
||||
"h": 720,
|
||||
"w": 1280
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"Radio": "./radio/radio.so",
|
||||
"Recorder": "./recorder/recorder.so",
|
||||
"Soapy": "./soapy/soapy.so",
|
||||
"FileSource": "./file_source/file_source.so",
|
||||
"RTLTCPSource": "./rtl_tcp_source/rtl_tcp_source.so"
|
||||
"Radio": "./radio/Release/radio.dll",
|
||||
"Recorder": "./recorder/Release/recorder.dll",
|
||||
"Soapy": "./soapy/Release/soapy.dll",
|
||||
"FileSource": "./file_source/Release/file_source.dll",
|
||||
"RTLTCPSource": "./rtl_tcp_source/Release/rtl_tcp_source.dll"
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue