From ec6d28441f2c61b385d717d5ce2e806448b7df6f Mon Sep 17 00:00:00 2001 From: chrisws Date: Fri, 9 Feb 2024 20:06:22 +1030 Subject: [PATCH 001/131] Update dependencies --- configure.ac | 6 ++++ gtk-server/uthash | 2 +- ioio/Makefile.am | 4 +-- nuklear/Nuklear | 2 +- raylib/README.md | 8 ++++- raylib/func-def.h | 6 ++++ raylib/func.h | 73 +++++++++++++++++++++++++++++++++++++++++++++- raylib/proc.h | 2 +- raylib/raygui | 2 +- raylib/raylib | 2 +- websocket/main.cpp | 14 ++++----- websocket/mongoose | 2 +- 12 files changed, 106 insertions(+), 17 deletions(-) diff --git a/configure.ac b/configure.ac index d4f1f4a..ffa572f 100644 --- a/configure.ac +++ b/configure.ac @@ -62,6 +62,8 @@ case "${host_os}" in WEBSOCKET_LDFLAGS="-lwsock32" GTK_SERVER_LDFLAGS="" GTK_SERVER_CPPFLAGS="-DGTK_SERVER_WIN32" + IOIO_CPPFLAGS="" + IOIO_LDFLAGS="" ;; *) @@ -72,6 +74,8 @@ case "${host_os}" in GTK_SERVER_LDFLAGS="`pkg-config --libs gtk+-3.0` -lXm -lXt" GTK_SERVER_CPPFLAGS="`pkg-config --cflags gtk+-3.0` -DGTK_SERVER_FFI -DGTK_SERVER_LIBRARY -DGTK_SERVER_UNIX -DGTK_SERVER_GTK3x" RAYLIB_LDFLAGS="-lGL -lm -lpthread -ldl -lrt -lX11" + IOIO_CPPFLAGS="-I/usr/lib/jvm/java-1.8.0-openjdk-amd64/include -I/usr/lib/jvm/java-1.8.0-openjdk-amd64/include/linux" + IOIO_LDFLAGS="-L/usr/lib/jvm/java-1.8.0-openjdk-amd64/jre/lib/amd64/server -ljvm" esac AC_SUBST(DEBUG_LDFLAGS) @@ -82,6 +86,8 @@ AC_SUBST(WEBSOCKET_LDFLAGS) AC_SUBST(PLATFORM_LDFLAGS) AC_SUBST(GTK_SERVER_LDFLAGS) AC_SUBST(GTK_SERVER_CPPFLAGS) +AC_SUBST(IOIO_LDFLAGS) +AC_SUBST(IOIO_CPPFLAGS) dnl change default aru setting to avoid warning ARFLAGS=cr diff --git a/gtk-server/uthash b/gtk-server/uthash index ca98384..eeba196 160000 --- a/gtk-server/uthash +++ b/gtk-server/uthash @@ -1 +1 @@ -Subproject commit ca98384ce7f30beb216f9a0bc88a3b4340ead729 +Subproject commit eeba1961f203869116a865e57c968e9c86e1b8c4 diff --git a/ioio/Makefile.am b/ioio/Makefile.am index 5650b4f..edbef39 100644 --- a/ioio/Makefile.am +++ b/ioio/Makefile.am @@ -19,11 +19,11 @@ all-am: $(generated) CLEANFILES = $(generated) AM_CXXFLAGS=-fno-rtti -std=c++14 -AM_CPPFLAGS = -I../include -I/usr/lib/jvm/java-1.8.0-openjdk-amd64/include -I/usr/lib/jvm/java-1.8.0-openjdk-amd64/include/linux -Wall +AM_CPPFLAGS = -I../include -Wall @IOIO_CPPFLAGS@ lib_LTLIBRARIES = libioio.la libioio_la_SOURCES = ../include/param.cpp ../include/hashmap.cpp ../include/apiexec.cpp main.cpp $(generated) -libioio_la_LDFLAGS = -module -rpath '$(libdir)' @PLATFORM_LDFLAGS@ -L/usr/lib/jvm/java-1.8.0-openjdk-amd64/jre/lib/amd64/server -ljvm +libioio_la_LDFLAGS = -module -rpath '$(libdir)' @PLATFORM_LDFLAGS@ @IOIO_LDFLAGS@ $(generated): api.json mkapi.bas mkdoc.bas $(sbasic) mkapi.bas > $@ diff --git a/nuklear/Nuklear b/nuklear/Nuklear index bed81e2..24d6a47 160000 --- a/nuklear/Nuklear +++ b/nuklear/Nuklear @@ -1 +1 @@ -Subproject commit bed81e2f4e810d833a56dd2139d353ed93924bee +Subproject commit 24d6a47d73fc538ecdc67bc1e3e7e98c56e2a4d5 diff --git a/raylib/README.md b/raylib/README.md index 67bdb35..9f4c101 100644 --- a/raylib/README.md +++ b/raylib/README.md @@ -4,7 +4,7 @@ raylib is a simple and easy-to-use library to enjoy videogames programming. https://www.raylib.com/ -Implemented APIs (604) +Implemented APIs (610) ---------------- | Name | Description | @@ -160,6 +160,7 @@ Implemented APIs (604) | func ExportImageAsCode(image, fileName) | Export image as code file defining an array of bytes, returns true on success | | func ExportImageToMemory(image, fileType, fileSize) | Export image to memory buffer | | func ExportMesh(mesh, fileName) | Export mesh data to file, returns true on success | +| func ExportMeshAsCode(mesh, fileName) | Export mesh as code file (.h) defining multiple arrays of vertex attributes | | func ExportWave(wave, fileName) | Export wave data to file, returns true on success | | func ExportWaveAsCode(wave, fileName) | Export wave sample data to code (.h), returns true on success | | func Fade(color, alpha) | Get color with alpha applied, alpha goes from 0.0f to 1.0f | @@ -262,6 +263,8 @@ Implemented APIs (604) | func GetScreenWidth() | Get current screen width | | func GetShaderLocation(shader, uniformName) | Get shader uniform location | | func GetShaderLocationAttrib(shader, attribName) | Get shader attribute location | +| func GetShapesTexture() | Get texture that is used for shapes drawing | +| func GetShapesTextureRectangle() | Get texture source rectangle that is used for shapes drawing | | func GetSplinePointBasis(p1, p2, p3, p4, t) | Get (evaluate) spline point: B-Spline | | func GetSplinePointBezierCubic(p1, c2, c3, p4, t) | Get (evaluate) spline point: Cubic Bezier | | func GetSplinePointBezierQuad(p1, c2, p3, t) | Get (evaluate) spline point: Quadratic Bezier | @@ -273,6 +276,7 @@ Implemented APIs (604) | func GetTouchPosition(index) | Get touch position XY for a touch point index (relative to screen size) | | func GetTouchX() | Get touch position X for touch point 0 (relative to screen size) | | func GetTouchY() | Get touch position Y for touch point 0 (relative to screen size) | +| func GetViewRay(mousePosition, camera, width, height) | Get a ray trace from mouse position in a viewport | | func GetWindowHandle() | Get native window handle | | func GetWindowPosition() | Get window position XY on monitor | | func GetWindowScaleDPI() | Get window scale DPI factor | @@ -425,6 +429,7 @@ Implemented APIs (604) | func LoadFontFromMemory(fileType, fileData, dataSize, fontSize, codepoints, codepointCount) | Load font from memory buffer, fileType refers to extension: i.e. '.ttf' | | func LoadImage(fileName) | Load image from file into CPU memory (RAM) | | func LoadImageAnim(fileName, frames) | Load image sequence from file (frames appended to image.data) | +| func LoadImageAnimFromMemory(fileType, fileData, dataSize, frames) | Load image sequence from memory buffer | | func LoadImageColors(image) | Load color data from image as a Color array (RGBA - 32bit) | | func LoadImageFromMemory(fileType, fileData, dataSize) | Load image from memory buffer, fileType refers to extension: i.e. '.png' | | func LoadImageFromScreen() | Load image from screen buffer and (screenshot) | @@ -566,6 +571,7 @@ Implemented APIs (604) | func TextLength(text) | Get text length, checks for '\\0' ending | | func TextReplace(text, replace, by) | Replace text string (WARNING: memory must be freed!) | | func TextSubtext(text, position, length) | Get a piece of a text string | +| func TextToFloat(text) | Get float value from text (negative values not supported) | | func TextToInteger(text) | Get integer value from text (negative values not supported) | | func TextToLower(text) | Get lower case version of provided string | | func TextToPascal(text) | Get Pascal case notation version of provided string | diff --git a/raylib/func-def.h b/raylib/func-def.h index eee6e9a..acffa42 100644 --- a/raylib/func-def.h +++ b/raylib/func-def.h @@ -34,6 +34,7 @@ {2, 2, "EXPORTIMAGEASCODE", cmd_exportimageascode}, {2, 2, "EXPORTIMAGETOMEMORY", cmd_exportimagetomemory}, {2, 2, "EXPORTMESH", cmd_exportmesh}, + {2, 2, "EXPORTMESHASCODE", cmd_exportmeshascode}, {2, 2, "EXPORTWAVE", cmd_exportwave}, {2, 2, "EXPORTWAVEASCODE", cmd_exportwaveascode}, {2, 2, "FADE", cmd_fade}, @@ -129,6 +130,8 @@ {0, 0, "GETSCREENWIDTH", cmd_getscreenwidth}, {2, 2, "GETSHADERLOCATION", cmd_getshaderlocation}, {2, 2, "GETSHADERLOCATIONATTRIB", cmd_getshaderlocationattrib}, + {0, 0, "GETSHAPESTEXTURE", cmd_getshapestexture}, + {0, 0, "GETSHAPESTEXTURERECTANGLE", cmd_getshapestexturerectangle}, {5, 5, "GETSPLINEPOINTBASIS", cmd_getsplinepointbasis}, {5, 5, "GETSPLINEPOINTBEZIERCUBIC", cmd_getsplinepointbeziercubic}, {4, 4, "GETSPLINEPOINTBEZIERQUAD", cmd_getsplinepointbezierquad}, @@ -140,6 +143,7 @@ {1, 1, "GETTOUCHPOSITION", cmd_gettouchposition}, {0, 0, "GETTOUCHX", cmd_gettouchx}, {0, 0, "GETTOUCHY", cmd_gettouchy}, + {4, 4, "GETVIEWRAY", cmd_getviewray}, {0, 0, "GETWINDOWHANDLE", cmd_getwindowhandle}, {0, 0, "GETWINDOWPOSITION", cmd_getwindowposition}, {0, 0, "GETWINDOWSCALEDPI", cmd_getwindowscaledpi}, @@ -209,6 +213,7 @@ {5, 5, "LOADFONTFROMMEMORY", cmd_loadfontfrommemory}, {1, 1, "LOADIMAGE", cmd_loadimage}, {1, 1, "LOADIMAGEANIM", cmd_loadimageanim}, + {3, 3, "LOADIMAGEANIMFROMMEMORY", cmd_loadimageanimfrommemory}, {1, 1, "LOADIMAGECOLORS", cmd_loadimagecolors}, {3, 3, "LOADIMAGEFROMMEMORY", cmd_loadimagefrommemory}, {0, 0, "LOADIMAGEFROMSCREEN", cmd_loadimagefromscreen}, @@ -246,6 +251,7 @@ {1, 1, "TEXTLENGTH", cmd_textlength}, {3, 3, "TEXTREPLACE", cmd_textreplace}, {3, 3, "TEXTSUBTEXT", cmd_textsubtext}, + {1, 1, "TEXTTOFLOAT", cmd_texttofloat}, {1, 1, "TEXTTOINTEGER", cmd_texttointeger}, {1, 1, "TEXTTOLOWER", cmd_texttolower}, {1, 1, "TEXTTOPASCAL", cmd_texttopascal}, diff --git a/raylib/func.h b/raylib/func.h index 9053aa7..a67ed58 100644 --- a/raylib/func.h +++ b/raylib/func.h @@ -452,6 +452,23 @@ static int cmd_exportmesh(int argc, slib_par_t *params, var_t *retval) { return result; } +// +// Export mesh as code file (.h) defining multiple arrays of vertex attributes +// +static int cmd_exportmeshascode(int argc, slib_par_t *params, var_t *retval) { + int result; + int mesh_id = get_mesh_id(argc, params, 0, retval); + if (mesh_id != -1) { + auto fileName = get_param_str(argc, params, 1, 0); + auto fnResult = ExportMeshAsCode(_meshMap.at(mesh_id), fileName); + v_setint(retval, fnResult); + result = 1; + } else { + result = 0; + } + return result; +} + // // Export wave data to file, returns true on success // @@ -1531,6 +1548,24 @@ static int cmd_getshaderlocationattrib(int argc, slib_par_t *params, var_t *retv return 1; } +// +// Get texture that is used for shapes drawing +// +static int cmd_getshapestexture(int argc, slib_par_t *params, var_t *retval) { + auto fnResult = GetShapesTexture(); + v_settexture2d(retval, fnResult); + return 1; +} + +// +// Get texture source rectangle that is used for shapes drawing +// +static int cmd_getshapestexturerectangle(int argc, slib_par_t *params, var_t *retval) { + auto fnResult = GetShapesTextureRectangle(); + v_setrect(retval, fnResult); + return 1; +} + // // Get (evaluate) spline point: B-Spline // @@ -1654,6 +1689,19 @@ static int cmd_gettouchy(int argc, slib_par_t *params, var_t *retval) { return 1; } +// +// Get a ray trace from mouse position in a viewport +// +static int cmd_getviewray(int argc, slib_par_t *params, var_t *retval) { + auto mousePosition = get_param_vec2(argc, params, 0); + auto camera = get_camera_3d(argc, params, 1); + auto width = get_param_num(argc, params, 2, 0); + auto height = get_param_num(argc, params, 3, 0); + auto fnResult = GetViewRay(mousePosition, camera, width, height); + v_setray(retval, fnResult); + return 1; +} + // // Get native window handle // @@ -2473,6 +2521,19 @@ static int cmd_loadimageanim(int argc, slib_par_t *params, var_t *retval) { return 1; } +// +// Load image sequence from memory buffer +// +static int cmd_loadimageanimfrommemory(int argc, slib_par_t *params, var_t *retval) { + auto fileType = get_param_str(argc, params, 0, 0); + auto fileData = (const unsigned char *)get_param_str(argc, params, 1, 0); + auto dataSize = get_param_int(argc, params, 2, 0); + auto frames = (int *)0; + auto fnResult = LoadImageAnimFromMemory(fileType, fileData, dataSize, frames); + v_setimage(retval, fnResult); + return 1; +} + // // Load color data from image as a Color array (RGBA - 32bit) // @@ -2918,7 +2979,7 @@ static int cmd_textlength(int argc, slib_par_t *params, var_t *retval) { // Replace text string (WARNING: memory must be freed!) // static int cmd_textreplace(int argc, slib_par_t *params, var_t *retval) { - auto text = (char *)get_param_str(argc, params, 0, 0); + auto text = get_param_str(argc, params, 0, 0); auto replace = get_param_str(argc, params, 1, 0); auto by = get_param_str(argc, params, 2, 0); auto fnResult = (const char *)TextReplace(text, replace, by); @@ -2938,6 +2999,16 @@ static int cmd_textsubtext(int argc, slib_par_t *params, var_t *retval) { return 1; } +// +// Get float value from text (negative values not supported) +// +static int cmd_texttofloat(int argc, slib_par_t *params, var_t *retval) { + auto text = get_param_str(argc, params, 0, 0); + auto fnResult = TextToFloat(text); + v_setreal(retval, fnResult); + return 1; +} + // // Get integer value from text (negative values not supported) // diff --git a/raylib/proc.h b/raylib/proc.h index 0e2627f..3869fff 100644 --- a/raylib/proc.h +++ b/raylib/proc.h @@ -2996,7 +2996,7 @@ static int cmd_unloadautomationeventlist(int argc, slib_par_t *params, var_t *re int result; int list_id = get_automationeventlist_id(argc, params, 0, retval); if (list_id != -1) { - UnloadAutomationEventList(&_automationEventListMap.at(list_id)); + UnloadAutomationEventList(_automationEventListMap.at(list_id)); _automationEventListMap.erase(list_id); result = 1; } else { diff --git a/raylib/raygui b/raylib/raygui index 141ae0c..7fe39be 160000 --- a/raylib/raygui +++ b/raylib/raygui @@ -1 +1 @@ -Subproject commit 141ae0cd312656b7764b01079e43fc887aaaf4ce +Subproject commit 7fe39be75af7d166c50afb6e6b9013398d66a7e1 diff --git a/raylib/raylib b/raylib/raylib index a91ebb7..dd8b561 160000 --- a/raylib/raylib +++ b/raylib/raylib @@ -1 +1 @@ -Subproject commit a91ebb75170780e28351226e11d34d347819fbc6 +Subproject commit dd8b5613cab0d62a2464be5e3b45e835adedea0c diff --git a/websocket/main.cpp b/websocket/main.cpp index 140227a..1d7c1cf 100644 --- a/websocket/main.cpp +++ b/websocket/main.cpp @@ -123,16 +123,16 @@ static void server_ev_close(mg_connection *conn, Session *session) { } } -static void server_handler(struct mg_connection *conn, int event, void *eventData, void *session) { +static void server_handler(struct mg_connection *conn, int event, void *eventData) { switch (event) { case MG_EV_HTTP_MSG: - server_http_msg(conn, (mg_http_message *)eventData, (Session *)session); + server_http_msg(conn, (mg_http_message *)eventData, (Session *)conn->fn_data); break; case MG_EV_WS_MSG: server_ws_msg(conn, (mg_ws_message *)eventData); break; case MG_EV_CLOSE: - server_ev_close(conn, (Session *)session); + server_ev_close(conn, (Session *)conn->fn_data); break; default: break; @@ -171,19 +171,19 @@ static void client_ws_msg(mg_connection *conn, mg_ws_message *message, Session * } } -static void client_handler(mg_connection *conn, int event, void *eventData, void *fnData) { +static void client_handler(mg_connection *conn, int event, void *eventData) { switch (event) { case MG_EV_ERROR: fprintf(stderr, "ERROR: [%p] %s", conn->fd, (char *)eventData); break; case MG_EV_WS_OPEN: - client_ws_open(conn, (Session *)fnData); + client_ws_open(conn, (Session *)conn->fn_data); break; case MG_EV_WS_MSG: - client_ws_msg(conn, (mg_ws_message *)eventData, (Session *)fnData); + client_ws_msg(conn, (mg_ws_message *)eventData, (Session *)conn->fn_data); break; case MG_EV_CLOSE: - ((Session *)fnData)->_state = kClosed; + ((Session *)conn->fn_data)->_state = kClosed; break; default: break; diff --git a/websocket/mongoose b/websocket/mongoose index d1b0342..1a5ea93 160000 --- a/websocket/mongoose +++ b/websocket/mongoose @@ -1 +1 @@ -Subproject commit d1b0342a1889f32fc876dd9ac68244613d38d286 +Subproject commit 1a5ea930ab73e2b727567026f6b990b1fc416c33 From 06bc2f7c9ba79031fba916294bea2f98de335bc9 Mon Sep 17 00:00:00 2001 From: chrisws Date: Tue, 13 Feb 2024 20:04:04 +1030 Subject: [PATCH 002/131] IOIO: validate SpiMaster pin values --- .../smallbasic/ioio/SpiMasterImpl.java | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/SpiMasterImpl.java b/ioio/src/main/java/net/sourceforge/smallbasic/ioio/SpiMasterImpl.java index a831cdf..6577d10 100644 --- a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/SpiMasterImpl.java +++ b/ioio/src/main/java/net/sourceforge/smallbasic/ioio/SpiMasterImpl.java @@ -1,12 +1,12 @@ package net.sourceforge.smallbasic.ioio; -import java.io.IOException; - import ioio.lib.api.IOIO; import ioio.lib.api.SpiMaster; import ioio.lib.api.exception.ConnectionLostException; import ioio.lib.spi.Log; +import java.io.IOException; + public class SpiMasterImpl extends IOTask { private static final String TAG = "SpiMasterImpl"; private final IOLock lock = new IOLock<>(); @@ -27,6 +27,7 @@ public void open(int miso, int mosi, int clk, int slaveSelect) throws IOExceptio this.mosi = mosi; this.clk = clk; this.slaveSelect = slaveSelect; + validatePins(); } public void write(int address, int data) { @@ -47,4 +48,27 @@ void setup(IOIO ioio) throws ConnectionLostException { Log.i(TAG, "setup entered: " + miso + " " + mosi + " " + clk + " " + slaveSelect); spiMaster = ioio.openSpiMaster(miso, mosi, clk, slaveSelect, SpiMaster.Rate.RATE_1M); } + + private void pinError(String name) { + setError("Incorrect " + name + " pin value"); + } + + private void validatePins() { + if (miso < 1) { + pinError("miso"); + } else if (mosi < 1) { + pinError("mosi"); + } else if (clk < 1) { + pinError("clk"); + } else if (slaveSelect < 1) { + pinError("slaveSelect"); + } else if (miso == mosi || + miso == clk || + miso == slaveSelect || + mosi == clk || + mosi == slaveSelect || + clk == slaveSelect) { + setError("One or pins have duplicate values"); + } + } } From dca593c1a54d125fdd71da814030e69a054e405c Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Tue, 13 Feb 2024 21:32:49 +1030 Subject: [PATCH 003/131] IOIO: partially implements TwiMaster --- ioio/main.cpp | 48 ++++++++++++++++--- .../smallbasic/ioio/DigitalInputImpl.java | 4 +- .../smallbasic/ioio/SpiMasterImpl.java | 10 +++- .../smallbasic/ioio/TwiMasterImpl.java | 38 +++++++++++++-- 4 files changed, 87 insertions(+), 13 deletions(-) diff --git a/ioio/main.cpp b/ioio/main.cpp index e8b6efd..9c0bb6b 100644 --- a/ioio/main.cpp +++ b/ioio/main.cpp @@ -133,6 +133,23 @@ struct IOTask { return result; } + // int foo(int) + int invokeIntInt(const char *name, int value, var_s *retval) { + int result = 0; + if (_instance != nullptr) { + jmethodID method = env->GetMethodID(_clazz, name, "(I)I"); + var_num_t value = 0; + if (method != nullptr) { + value = env->CallIntMethod(_instance, method, value); + } + if (!checkException(retval)) { + v_setint(retval, value); + result = 1; + } + } + return result; + } + // void foo(boolean) int invokeVoidBool(const char *name, int value, var_s *retval) { int result = 0; @@ -254,13 +271,31 @@ static int get_io_class_id(var_s *map, var_s *retval) { return result; } -static int cmd_twimaster_writeread(var_s *self, int argc, slib_par_t *arg, var_s *retval) { +static int cmd_twimaster_write(var_s *self, int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + if (argc != 2) { + error(retval, "TwiMaster.write", 2); + } else { + int id = get_io_class_id(self, retval); + if (id != -1) { + auto address = get_param_int(argc, arg, 0, 0); + auto data = get_param_int(argc, arg, 1, 0); + result = _ioTaskMap.at(id).invokeVoidInt2("write", address, data, retval); + } + } + return result; +} + +static int cmd_twimaster_read(var_s *self, int argc, slib_par_t *arg, var_s *retval) { int result = 0; - if (argc != 0) { - error(retval, "TwiMaster.writeRead", 0); + if (argc != 1) { + error(retval, "TwiMaster.read", 1); } else { - // TODO - //result = ioioTask->invokeVoidVoid("waitForDisconnect", retval); + int id = get_io_class_id(self, retval); + if (id != -1) { + auto address = get_param_int(argc, arg, 0, 0); + result = _ioTaskMap.at(id).invokeIntInt("read", address, retval); + } } return result; } @@ -281,7 +316,8 @@ static int cmd_spimaster_write(var_s *self, int argc, slib_par_t *arg, var_s *re } static void create_twimaster(var_t *map) { - v_create_callback(map, "writeRead", cmd_twimaster_writeread); + v_create_callback(map, "write", cmd_twimaster_write); + v_create_callback(map, "read", cmd_twimaster_read); } static void create_spimaster(var_t *map) { diff --git a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/DigitalInputImpl.java b/ioio/src/main/java/net/sourceforge/smallbasic/ioio/DigitalInputImpl.java index 32a4717..d929d86 100644 --- a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/DigitalInputImpl.java +++ b/ioio/src/main/java/net/sourceforge/smallbasic/ioio/DigitalInputImpl.java @@ -1,12 +1,12 @@ package net.sourceforge.smallbasic.ioio; +import java.util.concurrent.atomic.AtomicBoolean; + import ioio.lib.api.DigitalInput; import ioio.lib.api.IOIO; import ioio.lib.api.exception.ConnectionLostException; import ioio.lib.spi.Log; -import java.util.concurrent.atomic.AtomicBoolean; - public class DigitalInputImpl extends IOTask implements DigitalInput { private static final String TAG = "DigitalInput"; private final AtomicBoolean value = new AtomicBoolean(); diff --git a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/SpiMasterImpl.java b/ioio/src/main/java/net/sourceforge/smallbasic/ioio/SpiMasterImpl.java index 6577d10..bcd39d2 100644 --- a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/SpiMasterImpl.java +++ b/ioio/src/main/java/net/sourceforge/smallbasic/ioio/SpiMasterImpl.java @@ -1,12 +1,12 @@ package net.sourceforge.smallbasic.ioio; +import java.io.IOException; + import ioio.lib.api.IOIO; import ioio.lib.api.SpiMaster; import ioio.lib.api.exception.ConnectionLostException; import ioio.lib.spi.Log; -import java.io.IOException; - public class SpiMasterImpl extends IOTask { private static final String TAG = "SpiMasterImpl"; private final IOLock lock = new IOLock<>(); @@ -21,6 +21,12 @@ public SpiMasterImpl() { Log.i(TAG, "created"); } + @Override + public void close() { + super.close(); + spiMaster.close(); + } + public void open(int miso, int mosi, int clk, int slaveSelect) throws IOException { super.open(miso); this.miso = miso; diff --git a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java b/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java index 500eb5d..0609881 100644 --- a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java +++ b/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java @@ -1,5 +1,8 @@ package net.sourceforge.smallbasic.ioio; +import java.io.IOException; +import java.util.concurrent.atomic.AtomicInteger; + import ioio.lib.api.IOIO; import ioio.lib.api.TwiMaster; import ioio.lib.api.exception.ConnectionLostException; @@ -7,8 +10,9 @@ public class TwiMasterImpl extends IOTask { private static final String TAG = "TwiMasterImpl"; + private final IOLock lock = new IOLock<>(); + private final TwiMaster.Rate rate = TwiMaster.Rate.RATE_100KHz; private TwiMaster twiMaster = null; - private TwiMaster.Rate rate = TwiMaster.Rate.RATE_100KHz; private int twiNum; private boolean smbus; @@ -17,14 +21,42 @@ public TwiMasterImpl() { Log.i(TAG, "created"); } - public void open(int twiNum, int smbus) { + @Override + public void close() { + super.close(); + twiMaster.close(); + twiMaster = null; + } + + public void open(int twiNum, int smbus) throws IOException { + super.open(twiNum); this.twiNum = twiNum; this.smbus = (smbus == 1); } + public int read(int address) { + handleError(); + AtomicInteger atomicLong = new AtomicInteger(); + lock.invoke((i) -> { + byte[] buffer = new byte[4]; + twiMaster.writeRead(address, false, null, 0, buffer, 4); + int value = 0; // TODO read buffer into value + atomicLong.set(value); + }); + return atomicLong.get(); + } + + public void write(int address, int data) { + handleError(); + lock.invoke((i) -> { + byte[] buffer = {(byte) data}; + twiMaster.writeRead(address, false, buffer, buffer.length, null, 0); + }); + } + @Override void loop() throws ConnectionLostException, InterruptedException { - // TODO + lock.process(twiMaster); } @Override From c05ffefef73b3dee7d96aa078ea9d8dbdb7116bb Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Thu, 15 Feb 2024 07:18:40 +1030 Subject: [PATCH 004/131] IOIO: implements veml6030 light sensor sample --- ioio/main.cpp | 93 +++++++++++++------ ioio/samples/veml6030.bas | 32 +++++++ .../smallbasic/ioio/TwiMasterImpl.java | 24 +++-- 3 files changed, 107 insertions(+), 42 deletions(-) create mode 100644 ioio/samples/veml6030.bas diff --git a/ioio/main.cpp b/ioio/main.cpp index 9c0bb6b..4b8abc6 100644 --- a/ioio/main.cpp +++ b/ioio/main.cpp @@ -133,23 +133,6 @@ struct IOTask { return result; } - // int foo(int) - int invokeIntInt(const char *name, int value, var_s *retval) { - int result = 0; - if (_instance != nullptr) { - jmethodID method = env->GetMethodID(_clazz, name, "(I)I"); - var_num_t value = 0; - if (method != nullptr) { - value = env->CallIntMethod(_instance, method, value); - } - if (!checkException(retval)) { - v_setint(retval, value); - result = 1; - } - } - return result; - } - // void foo(boolean) int invokeVoidBool(const char *name, int value, var_s *retval) { int result = 0; @@ -238,6 +221,61 @@ struct IOTask { return result; } + // int readWrite(int address, byte[] write) { + int invokeReadWrite(int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + if (_instance != nullptr) { + jmethodID method = env->GetMethodID(_clazz, "readWrite", "(I[B)I"); + var_num_t value = 0; + if (method != nullptr) { + jbyteArray array = env->NewByteArray(argc - 1); + jbyte *elements = env->GetByteArrayElements(array, nullptr); + populateByteArray(argc, arg, elements); + auto address = get_param_int(argc, arg, 0, 0); + + value = env->CallIntMethod(_instance, method, address, array); + releaseArray(array, elements); + } + if (!checkException(retval)) { + v_setint(retval, value); + result = 1; + } + } + return result; + } + + // int readWrite(int address, byte[] write) { + int invokeWrite(int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + if (_instance != nullptr) { + jmethodID method = env->GetMethodID(_clazz, "write", "(I[B)V"); + if (method != nullptr) { + jbyteArray array = env->NewByteArray(argc - 1); + jbyte *elements = env->GetByteArrayElements(array, nullptr); + populateByteArray(argc, arg, elements); + auto address = get_param_int(argc, arg, 0, 0); + + env->CallVoidMethod(_instance, method, address, array); + releaseArray(array, elements); + } + if (!checkException(retval)) { + result = 1; + } + } + return result; + } + + void populateByteArray(int argc, slib_par_t *arg, jbyte *elements) { + for (int i = 1; i < argc; i++) { + elements[i] = get_param_int(argc, arg, i, 0); + } + } + + void releaseArray(jbyteArray array, jbyte *elements) { + env->ReleaseByteArrayElements(array, elements, 0); + env->DeleteLocalRef(array); + } + int open(int pin, var_s *retval) { return invokeVoidInt("open", pin, retval); } @@ -271,30 +309,27 @@ static int get_io_class_id(var_s *map, var_s *retval) { return result; } -static int cmd_twimaster_write(var_s *self, int argc, slib_par_t *arg, var_s *retval) { +static int cmd_twimaster_readwrite(var_s *self, int argc, slib_par_t *arg, var_s *retval) { int result = 0; - if (argc != 2) { - error(retval, "TwiMaster.write", 2); + if (argc < 2) { + error(retval, "TwiMaster.readWrite", 2, 10); } else { int id = get_io_class_id(self, retval); if (id != -1) { - auto address = get_param_int(argc, arg, 0, 0); - auto data = get_param_int(argc, arg, 1, 0); - result = _ioTaskMap.at(id).invokeVoidInt2("write", address, data, retval); + result = _ioTaskMap.at(id).invokeReadWrite(argc, arg, retval); } } return result; } -static int cmd_twimaster_read(var_s *self, int argc, slib_par_t *arg, var_s *retval) { +static int cmd_twimaster_write(var_s *self, int argc, slib_par_t *arg, var_s *retval) { int result = 0; - if (argc != 1) { - error(retval, "TwiMaster.read", 1); + if (argc < 2) { + error(retval, "TwiMaster.write", 2, 10); } else { int id = get_io_class_id(self, retval); if (id != -1) { - auto address = get_param_int(argc, arg, 0, 0); - result = _ioTaskMap.at(id).invokeIntInt("read", address, retval); + result = _ioTaskMap.at(id).invokeWrite(argc, arg, retval); } } return result; @@ -317,7 +352,7 @@ static int cmd_spimaster_write(var_s *self, int argc, slib_par_t *arg, var_s *re static void create_twimaster(var_t *map) { v_create_callback(map, "write", cmd_twimaster_write); - v_create_callback(map, "read", cmd_twimaster_read); + v_create_callback(map, "readWrite", cmd_twimaster_readwrite); } static void create_spimaster(var_t *map) { diff --git a/ioio/samples/veml6030.bas b/ioio/samples/veml6030.bas new file mode 100644 index 0000000..17703ee --- /dev/null +++ b/ioio/samples/veml6030.bas @@ -0,0 +1,32 @@ +rem +rem https://learn.sparkfun.com/tutorials/qwiic-ambient-light-sensor-veml6030-hookup-guide/all +rem https://piico.dev/p3 +rem PiicoDev Ambient Light Sensor VEML6030 +rem + +import ioio + +rem i2c address +const address = 0x10 + +rem register where the light sensing data is stored +const alsDataReg = 0x04 + +rem ambient light sensing configuration register +const alsConfReg = 0 + +rem default settings +rem initialise gain:1x, integration 100ms, persistence 1, disable interrupt +const alsConf = 0 + +p3 = ioio.openTwiMaster(1, 0) + +ioio.waitForConnect(10) + +rem configure default settings +p3.write(address, alsConfReg, alsConf) + +while 1 + print p3.readWrite(address, alsDataReg) + delay 2000 +wend diff --git a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java b/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java index 0609881..89af70d 100644 --- a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java +++ b/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java @@ -1,7 +1,6 @@ package net.sourceforge.smallbasic.ioio; import java.io.IOException; -import java.util.concurrent.atomic.AtomicInteger; import ioio.lib.api.IOIO; import ioio.lib.api.TwiMaster; @@ -34,23 +33,22 @@ public void open(int twiNum, int smbus) throws IOException { this.smbus = (smbus == 1); } - public int read(int address) { + public int readWrite(int address, byte[] write) { handleError(); - AtomicInteger atomicLong = new AtomicInteger(); - lock.invoke((i) -> { - byte[] buffer = new byte[4]; - twiMaster.writeRead(address, false, null, 0, buffer, 4); - int value = 0; // TODO read buffer into value - atomicLong.set(value); + return lock.invokeInt((i) -> { + byte[] read = new byte[4]; + twiMaster.writeRead(address, false, write, write.length, read, read.length); + Log.i(TAG, "read = " + read.length + " " + address + " " + + read[0] + " " + read[1] + " " + read[2] + " " + read[3]); + int value = 0; // TODO read read into value + return value; }); - return atomicLong.get(); } - public void write(int address, int data) { + public void write(int address, byte[] write) { handleError(); lock.invoke((i) -> { - byte[] buffer = {(byte) data}; - twiMaster.writeRead(address, false, buffer, buffer.length, null, 0); + twiMaster.writeRead(address, false, write, write.length, null, 0); }); } @@ -61,7 +59,7 @@ void loop() throws ConnectionLostException, InterruptedException { @Override void setup(IOIO ioio) throws ConnectionLostException { - Log.i(TAG, "setup entered"); + Log.i(TAG, "setup entered: " + twiNum); twiMaster = ioio.openTwiMaster(twiNum, rate, smbus); } } From e954f082ef3d217d7c2712d951bb27f866be116f Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Thu, 15 Feb 2024 21:35:58 +1030 Subject: [PATCH 005/131] IOIO: implements veml6030 light sensor sample --- ioio/main.cpp | 52 ++++++++++--------- ioio/samples/veml6030.bas | 10 ++-- .../smallbasic/ioio/TwiMasterImpl.java | 15 +++--- 3 files changed, 40 insertions(+), 37 deletions(-) diff --git a/ioio/main.cpp b/ioio/main.cpp index 4b8abc6..5cfaec1 100644 --- a/ioio/main.cpp +++ b/ioio/main.cpp @@ -33,13 +33,22 @@ int nextId = 1; #define CLASS_SPIMASTER "net/sourceforge/smallbasic/ioio/SpiMasterImpl" #define CLASS_IOIO "net/sourceforge/smallbasic/ioio/IOIOImpl" #define CLASS_IOTASK_ID 1 +#define ARRAY_SIZE 10 struct IOTask { - IOTask(): _clazz(nullptr), _instance(nullptr) {} + IOTask(): + _clazz(nullptr), + _instance(nullptr), + _array(nullptr) { + } virtual ~IOTask() { + if (_array) { + env->DeleteLocalRef(_array); + } _clazz = nullptr; _instance = nullptr; + _array = nullptr; } bool create(const char *path) { @@ -225,16 +234,12 @@ struct IOTask { int invokeReadWrite(int argc, slib_par_t *arg, var_s *retval) { int result = 0; if (_instance != nullptr) { - jmethodID method = env->GetMethodID(_clazz, "readWrite", "(I[B)I"); + jmethodID method = env->GetMethodID(_clazz, "readWrite", "(I[BI)I"); var_num_t value = 0; if (method != nullptr) { - jbyteArray array = env->NewByteArray(argc - 1); - jbyte *elements = env->GetByteArrayElements(array, nullptr); - populateByteArray(argc, arg, elements); auto address = get_param_int(argc, arg, 0, 0); - - value = env->CallIntMethod(_instance, method, address, array); - releaseArray(array, elements); + populateByteArray(argc, arg, 1); + value = env->CallIntMethod(_instance, method, address, _array, argc - 1); } if (!checkException(retval)) { v_setint(retval, value); @@ -248,15 +253,11 @@ struct IOTask { int invokeWrite(int argc, slib_par_t *arg, var_s *retval) { int result = 0; if (_instance != nullptr) { - jmethodID method = env->GetMethodID(_clazz, "write", "(I[B)V"); + jmethodID method = env->GetMethodID(_clazz, "write", "(I[BI)V"); if (method != nullptr) { - jbyteArray array = env->NewByteArray(argc - 1); - jbyte *elements = env->GetByteArrayElements(array, nullptr); - populateByteArray(argc, arg, elements); auto address = get_param_int(argc, arg, 0, 0); - - env->CallVoidMethod(_instance, method, address, array); - releaseArray(array, elements); + populateByteArray(argc, arg, 1); + env->CallVoidMethod(_instance, method, address, _array, argc - 1); } if (!checkException(retval)) { result = 1; @@ -265,15 +266,15 @@ struct IOTask { return result; } - void populateByteArray(int argc, slib_par_t *arg, jbyte *elements) { - for (int i = 1; i < argc; i++) { - elements[i] = get_param_int(argc, arg, i, 0); + void populateByteArray(int argc, slib_par_t *arg, int offset) { + if (!_array) { + _array = env->NewByteArray(ARRAY_SIZE); } - } - - void releaseArray(jbyteArray array, jbyte *elements) { - env->ReleaseByteArrayElements(array, elements, 0); - env->DeleteLocalRef(array); + jbyte *elements = env->GetByteArrayElements(_array, nullptr); + for (int i = offset, j = 0; i < argc && i < ARRAY_SIZE; i++, j++) { + elements[j] = get_param_int(argc, arg, i, 0); + } + env->ReleaseByteArrayElements(_array, elements, 0); } int open(int pin, var_s *retval) { @@ -291,6 +292,7 @@ struct IOTask { private: jclass _clazz; jobject _instance; + jbyteArray _array; }; robin_hood::unordered_map _ioTaskMap; @@ -312,7 +314,7 @@ static int get_io_class_id(var_s *map, var_s *retval) { static int cmd_twimaster_readwrite(var_s *self, int argc, slib_par_t *arg, var_s *retval) { int result = 0; if (argc < 2) { - error(retval, "TwiMaster.readWrite", 2, 10); + error(retval, "TwiMaster.readWrite", 2, ARRAY_SIZE); } else { int id = get_io_class_id(self, retval); if (id != -1) { @@ -325,7 +327,7 @@ static int cmd_twimaster_readwrite(var_s *self, int argc, slib_par_t *arg, var_s static int cmd_twimaster_write(var_s *self, int argc, slib_par_t *arg, var_s *retval) { int result = 0; if (argc < 2) { - error(retval, "TwiMaster.write", 2, 10); + error(retval, "TwiMaster.write", 2, ARRAY_SIZE); } else { int id = get_io_class_id(self, retval); if (id != -1) { diff --git a/ioio/samples/veml6030.bas b/ioio/samples/veml6030.bas index 17703ee..31ea6f6 100644 --- a/ioio/samples/veml6030.bas +++ b/ioio/samples/veml6030.bas @@ -1,6 +1,7 @@ rem rem https://learn.sparkfun.com/tutorials/qwiic-ambient-light-sensor-veml6030-hookup-guide/all rem https://piico.dev/p3 +rem https://www.vishay.com/docs/84366/veml6030.pdf rem PiicoDev Ambient Light Sensor VEML6030 rem @@ -12,6 +13,9 @@ const address = 0x10 rem register where the light sensing data is stored const alsDataReg = 0x04 +rem measure the total brightness of the ambient light regardless of its color +const whiteDataReg = 0x05 + rem ambient light sensing configuration register const alsConfReg = 0 @@ -26,7 +30,7 @@ ioio.waitForConnect(10) rem configure default settings p3.write(address, alsConfReg, alsConf) -while 1 +for i = 0 to 10 print p3.readWrite(address, alsDataReg) - delay 2000 -wend + delay 1000 +next diff --git a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java b/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java index 89af70d..72b52ae 100644 --- a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java +++ b/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java @@ -33,22 +33,19 @@ public void open(int twiNum, int smbus) throws IOException { this.smbus = (smbus == 1); } - public int readWrite(int address, byte[] write) { + public int readWrite(int address, final byte[] write, int len) { handleError(); return lock.invokeInt((i) -> { - byte[] read = new byte[4]; - twiMaster.writeRead(address, false, write, write.length, read, read.length); - Log.i(TAG, "read = " + read.length + " " + address + " " - + read[0] + " " + read[1] + " " + read[2] + " " + read[3]); - int value = 0; // TODO read read into value - return value; + byte[] read = new byte[2]; + twiMaster.writeRead(address, false, write, len, read, read.length); + return (read[1] << 8) + read[0]; }); } - public void write(int address, byte[] write) { + public void write(int address, final byte[] write, int len) { handleError(); lock.invoke((i) -> { - twiMaster.writeRead(address, false, write, write.length, null, 0); + twiMaster.writeRead(address, false, write, len, null, 0); }); } From 12f1c7f875345a676197fb05114ea68c2c6f8fc3 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Sat, 17 Feb 2024 07:15:39 +1030 Subject: [PATCH 006/131] IOIO: write/readWrite can now pass array as well as var args --- ioio/main.cpp | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/ioio/main.cpp b/ioio/main.cpp index 5cfaec1..b7695c3 100644 --- a/ioio/main.cpp +++ b/ioio/main.cpp @@ -187,6 +187,7 @@ struct IOTask { return result; } + // void foo(int, int) int invokeVoidInt2(const char *name, int value1, int value2, var_s *retval) { int result = 0; if (_instance != nullptr) { @@ -201,6 +202,7 @@ struct IOTask { return result; } + // void foo(int, int, int, int) int invokeVoidInt4(const char *name, int value1, int value2, int value3, int value4, var_s *retval) { int result = 0; if (_instance != nullptr) { @@ -249,7 +251,7 @@ struct IOTask { return result; } - // int readWrite(int address, byte[] write) { + // int write(int address, byte[] write) { int invokeWrite(int argc, slib_par_t *arg, var_s *retval) { int result = 0; if (_instance != nullptr) { @@ -266,13 +268,24 @@ struct IOTask { return result; } - void populateByteArray(int argc, slib_par_t *arg, int offset) { + // populate the java byte array with the contents of the basic array + void populateByteArray(int argc, slib_par_t *params, int offset) { if (!_array) { _array = env->NewByteArray(ARRAY_SIZE); } jbyte *elements = env->GetByteArrayElements(_array, nullptr); - for (int i = offset, j = 0; i < argc && i < ARRAY_SIZE; i++, j++) { - elements[j] = get_param_int(argc, arg, i, 0); + if ((argc - offset) == 1 && is_param_array(argc, params, offset)) { + // argument is an array (assume of ints) + var_s *array = params[offset].var_p; + int size = v_asize(array); + for (int i = 0; i < size && i < ARRAY_SIZE; i++) { + var_s *elem = v_elem(array, i); + elements[i] = v_is_type(elem, V_INT) ? elem->v.i : 0; + } + } else { + for (int i = offset, j = 0; i < argc && i < ARRAY_SIZE; i++, j++) { + elements[j] = get_param_int(argc, params, i, 0); + } } env->ReleaseByteArrayElements(_array, elements, 0); } From 95105ec8b8682cf720d272ad7bdbc9a50b6632f0 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Sun, 18 Feb 2024 16:39:05 +1030 Subject: [PATCH 007/131] IOIO: twi.readWrite now takes number of bytes to read argument --- ioio/main.cpp | 14 ++++++++------ ioio/samples/veml6030.bas | 4 ++-- .../net/sourceforge/smallbasic/ioio/IOLock.java | 17 ++++++++++++++++- .../smallbasic/ioio/TwiMasterImpl.java | 14 +++++++++----- 4 files changed, 35 insertions(+), 14 deletions(-) diff --git a/ioio/main.cpp b/ioio/main.cpp index b7695c3..25913d4 100644 --- a/ioio/main.cpp +++ b/ioio/main.cpp @@ -236,12 +236,13 @@ struct IOTask { int invokeReadWrite(int argc, slib_par_t *arg, var_s *retval) { int result = 0; if (_instance != nullptr) { - jmethodID method = env->GetMethodID(_clazz, "readWrite", "(I[BI)I"); - var_num_t value = 0; + jmethodID method = env->GetMethodID(_clazz, "readWrite", "(II[BI)J"); + var_int_t value = 0; if (method != nullptr) { auto address = get_param_int(argc, arg, 0, 0); - populateByteArray(argc, arg, 1); - value = env->CallIntMethod(_instance, method, address, _array, argc - 1); + auto readBytes = get_param_int(argc, arg, 1, 2); + populateByteArray(argc, arg, 2); + value = env->CallIntMethod(_instance, method, address, readBytes, _array, argc - 1); } if (!checkException(retval)) { v_setint(retval, value); @@ -280,13 +281,14 @@ struct IOTask { int size = v_asize(array); for (int i = 0; i < size && i < ARRAY_SIZE; i++) { var_s *elem = v_elem(array, i); - elements[i] = v_is_type(elem, V_INT) ? elem->v.i : 0; + elements[i] = v_is_type(elem, V_INT) ? elem->v.i : elem->v.n; } } else { for (int i = offset, j = 0; i < argc && i < ARRAY_SIZE; i++, j++) { elements[j] = get_param_int(argc, params, i, 0); } } + // make the changes available to the java side env->ReleaseByteArrayElements(_array, elements, 0); } @@ -327,7 +329,7 @@ static int get_io_class_id(var_s *map, var_s *retval) { static int cmd_twimaster_readwrite(var_s *self, int argc, slib_par_t *arg, var_s *retval) { int result = 0; if (argc < 2) { - error(retval, "TwiMaster.readWrite", 2, ARRAY_SIZE); + error(retval, "TwiMaster.readWrite(address, bytes, [data]", 2, ARRAY_SIZE); } else { int id = get_io_class_id(self, retval); if (id != -1) { diff --git a/ioio/samples/veml6030.bas b/ioio/samples/veml6030.bas index 31ea6f6..77c978f 100644 --- a/ioio/samples/veml6030.bas +++ b/ioio/samples/veml6030.bas @@ -30,7 +30,7 @@ ioio.waitForConnect(10) rem configure default settings p3.write(address, alsConfReg, alsConf) -for i = 0 to 10 - print p3.readWrite(address, alsDataReg) +for i = 0 to 5 + print p3.readWrite(address, 2, [alsDataReg]) delay 1000 next diff --git a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOLock.java b/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOLock.java index 1366f91..9cfd29e 100644 --- a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOLock.java +++ b/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOLock.java @@ -1,6 +1,8 @@ package net.sourceforge.smallbasic.ioio; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import ioio.lib.api.exception.ConnectionLostException; @@ -36,7 +38,20 @@ public float invoke(Function function) { public int invokeInt(Function function) { CountDownLatch latch = beginLatch(); - AtomicReference result = new AtomicReference<>(); + AtomicInteger result = new AtomicInteger(); + synchronized (mutex) { + this.consumer = (i) -> { + result.set(function.apply(i)); + latch.countDown(); + }; + } + endLatch(latch); + return result.get(); + } + + public long invokeLong(Function function) { + CountDownLatch latch = beginLatch(); + AtomicLong result = new AtomicLong(); synchronized (mutex) { this.consumer = (i) -> { result.set(function.apply(i)); diff --git a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java b/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java index 72b52ae..478661a 100644 --- a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java +++ b/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java @@ -33,12 +33,16 @@ public void open(int twiNum, int smbus) throws IOException { this.smbus = (smbus == 1); } - public int readWrite(int address, final byte[] write, int len) { + public long readWrite(int address, int readLen, final byte[] write, int writeLen) { handleError(); - return lock.invokeInt((i) -> { - byte[] read = new byte[2]; - twiMaster.writeRead(address, false, write, len, read, read.length); - return (read[1] << 8) + read[0]; + return lock.invokeLong((i) -> { + byte[] read = new byte[readLen]; + twiMaster.writeRead(address, false, write, writeLen, read, read.length); + long result = 0; + for (int index = 0; index < read.length; index++) { + result += ((long)read[index]) << (index * 8); + } + return result; }); } From bb751ef94f4508a779985f3b884ce362c5800410 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Mon, 19 Feb 2024 19:49:51 +1030 Subject: [PATCH 008/131] IOIO: added bme280 sample (giving incorrect output) --- ioio/samples/bme280.bas | 179 ++++++++++++++++++++++++++++++++++++++ ioio/samples/veml6030.bas | 8 +- 2 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 ioio/samples/bme280.bas diff --git a/ioio/samples/bme280.bas b/ioio/samples/bme280.bas new file mode 100644 index 0000000..97e4e0e --- /dev/null +++ b/ioio/samples/bme280.bas @@ -0,0 +1,179 @@ +rem +rem https://piico.dev/p2 +rem https://www.mouser.com/datasheet/2/783/BST-BME280-DS002-1509607.pdf +rem +rem based on:https://raw.githubusercontent.com/CoreElectronics/CE-PiicoDev-BME280-MicroPython-Module/main/PiicoDev_BME280.py +rem Original header: +rem A MicroPython class for the Core Electronics PiicoDev Atmospheric Sensor BME280 +rem Ported by Michael Ruppe at Core Electronics +rem MAR 2021 +rem Original repo https://bit.ly/2yJwysL +rem +rem wiring: +rem SDA -> pin 1 +rem CLK -> pin 2 +rem + +import ioio + +const address = 0x77 +const t_mode = 2 +const p_mode = 5 +const h_mode = 1 +const iir = 1 + +sensor = ioio.openTwiMaster(1, 0) + +ioio.waitForConnect(10) + +sdata = bme280_init(t_mode, p_mode, h_mode, iir) +for i = 0 to 10 + print values(sdata) + delay 1000 +next i + +rem the code below could be moved to a unit +func short(dat) + if dat > 32767 then + return dat - 65536 + else + return dat + endif +end + +func read8(register) + return sensor.readWrite(address, 1, register) +end + +func read16(register) + return sensor.readWrite(address, 2, register) +end + +func write8(register, dat) + sensor.write(address, register, dat) +end + +func bme280_init(t_mode, p_mode, h_mode, iir) + local result = {} + result.t_mode = t_mode + result.p_mode = p_mode + result.h_mode = h_mode + result.iir = iir + result.t_fine = 0 + result.T1 = read16(0x88) + result.T2 = short(read16(0x8A)) + result.T3 = short(read16(0x8C)) + result.P1 = read16(0x8E) + result.P2 = short(read16(0x90)) + result.P3 = short(read16(0x92)) + result.P4 = short(read16(0x94)) + result.P5 = short(read16(0x96)) + result.P6 = short(read16(0x98)) + result.P7 = short(read16(0x9A)) + result.P8 = short(read16(0x9C)) + result.P9 = short(read16(0x9E)) + result.H1 = read8(0xA1) + result.H2 = short(read16(0xE1)) + result.H3 = read8(0xE3) + + local a = read8(0xE5) + result.H4 = (read8(0xE4) lshift 4) + (a mod 16) + result.H5 = (read8(0xE6) lshift 4) + (a rshift 4) + result.H6 = read8(0xE7) + if H6 > 127 then result.H6 -= 256 + + write8(0xF2, h_mode) + sleep_ms(10) + write8(0xF4, 0x24) + sleep_ms(10) + write8(0xF5, iir lshift 2) + return result +end + +func read_raw_data(sdata) + write8(0xF4, (sdata.p_mode lshift 5 | sdata.t_mode lshift 2 | 1)) + sleep_time = 1250 + + if sdata.t_mode in [1, 2, 3, 4, 5] then + sleep_time += 2300*(1 lshift sdata.t_mode) + endif + if sdata.p_mode in [1, 2, 3, 4, 5] then + sleep_time += 575+(2300*(1 lshift sdata.p_mode)) + endif + if sdata.h_mode in [1, 2, 3, 4, 5] then + sleep_time += 575+(2300*(1 lshift sdata.h_mode)) + endif + + sleep_ms(1 + sleep_time / 1000) + + while (read16(0xF3) & 0x08) + sleep_ms(1) + wend + + local raw_p = ((read8(0xF7) lshift 16) | (read8(0xF8) lshift 8) | read8(0xF9)) rshift 4 + local raw_t = ((read8(0xFA) lshift 16) | (read8(0xFB) lshift 8) | read8(0xFC)) rshift 4 + local raw_h = (read8(0xFD) lshift 8)| read8(0xFE) + return [raw_t, raw_p, raw_h] +end + +rem yikes !!!! +func read_compensated_data(sdata) + local raw_t, raw_p, raw_h, var1, var2, var3, h + + [raw_t, raw_p, raw_h] = read_raw_data(sdata) + + var1 = ((raw_t rshift 3) - (sdata.T1 lshift 1)) * (sdata.T2 rshift 11) + var2 = (raw_t rshift 4) - sdata.T1 + var2 = var2*((raw_t rshift 4) - sdata.T1) + var2 = ((var2 rshift 12) * sdata.T3) rshift 14 + sdata.t_fine = var1+var2 + + local temp = (sdata.t_fine * 5 + 128) rshift 8 + var1 = sdata.t_fine - 128000 + var2 = var1 * var1 * sdata.P6 + var2 = var2 + ((var1*sdata.P5) lshift 17) + var2 = var2 + (sdata.P4 lshift 35) + var1 = (((var1 * var1 * sdata.P3) rshift 8) + ((var1 * sdata.P2) lshift 12)) + var1 = (((1 lshift 47)+var1)*sdata.P1) rshift 33 + if var1 == 0 then + pres = 0 + else + p = ((((1048576-raw_p) lshift 31)-var2) * 3125) / var1 + var1 = (sdata.P9 * (p rshift 13) * (p rshift 13)) rshift 25 + var2 = (sdata.P8 * p) rshift 19 + pres = ((p + var1 + var2) rshift 8) + (sdata.P7 lshift 4) + endif + + h = sdata.t_fine - 76800 + h = (((((raw_h lshift 14) - (sdata.H4 lshift 20) - (sdata.H5 * h)) + 16384) rshift 15) * & + (((((((h * sdata.H6) rshift 10) * (((h * sdata.H3) rshift 11) + 32768)) rshift 10) + 2097152) * sdata.H2 + 8192) rshift 14)) + h = h - (((((h rshift 15) * (h rshift 15)) rshift 7) * sdata.H1) rshift 4) + h = iff(h < 0, 0, h) + h = iff(h > 419430400, 419430400, h) + humi = h rshift 12 + return [temp, pres, humi] +end + +func values(sdata) + local temp, pres, humi + [temp, pres, humi] = read_compensated_data(sdata) + return [temp / 100, pres / 256, humi / 1024] +end + +func pressure_precision(sdata) + local p = read_compensated_data(sdata)[1] + local pi = p / 256 + local pd = (p % 256) / 256 + return [pi, pd] +end + +func altitude(sdata) + local pi, pd + local pressure_sea_level=1013.25 + [pi, pd] = pressure_precision(sdata) + return 44330 * (1 - (((pi + pd) / 100) / pressure_sea_level) * (1 / 5.255)) +end + +sub sleep_ms(ms) + delay ms +end diff --git a/ioio/samples/veml6030.bas b/ioio/samples/veml6030.bas index 77c978f..2759a6f 100644 --- a/ioio/samples/veml6030.bas +++ b/ioio/samples/veml6030.bas @@ -7,6 +7,12 @@ rem import ioio +rem +rem wiring: +rem SDA -> pin 1 +rem CLK -> pin 2 +rem + rem i2c address const address = 0x10 @@ -28,7 +34,7 @@ p3 = ioio.openTwiMaster(1, 0) ioio.waitForConnect(10) rem configure default settings -p3.write(address, alsConfReg, alsConf) +p3.write(address, [alsConfReg, alsConf]) for i = 0 to 5 print p3.readWrite(address, 2, [alsDataReg]) From 8b261638716f6e0d0961bc0c652a587452cc1035 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Tue, 20 Feb 2024 20:49:07 +1030 Subject: [PATCH 009/131] IOIO: fix reading multi-byte Twi values --- ioio/main.cpp | 5 +++-- .../java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ioio/main.cpp b/ioio/main.cpp index 25913d4..57e5c9b 100644 --- a/ioio/main.cpp +++ b/ioio/main.cpp @@ -410,14 +410,15 @@ SBLIB_API int sblib_func_count() { int sblib_init(const char *sourceFile) { JavaVMInitArgs vm_args; - JavaVMOption options[2]; + JavaVMOption options[3]; options[0].optionString = (char *)"-Djava.class.path=./target/ioio-1.0-jar-with-dependencies.jar"; options[1].optionString = (char *)"-Dioio.SerialPorts=IOIO0"; + options[2].optionString = (char *)"-Xrs"; //options[2].optionString = "-Xdebug"; //options[3].optionString = "-agentlib:jdwp=transport=dt_socket,server=y,address=5005,suspend=y"; //options[2].optionString = (char *)"-Xcheck:jni"; vm_args.version = JNI_VERSION_1_8; - vm_args.nOptions = 2; + vm_args.nOptions = 3; vm_args.options = options; vm_args.ignoreUnrecognized = 0; int result = (JNI_CreateJavaVM(&jvm, (void **)&env, &vm_args) == JNI_OK && diff --git a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java b/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java index 478661a..b736c67 100644 --- a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java +++ b/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java @@ -40,7 +40,7 @@ public long readWrite(int address, int readLen, final byte[] write, int writeLen twiMaster.writeRead(address, false, write, writeLen, read, read.length); long result = 0; for (int index = 0; index < read.length; index++) { - result += ((long)read[index]) << (index * 8); + result += ((long)Byte.toUnsignedInt(read[index])) << (index * 8); } return result; }); From a1523529b1442dcf027116a73af1d71b857a7c05 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Wed, 21 Feb 2024 08:50:26 +1030 Subject: [PATCH 010/131] IOIO: added error checking for twi-readwrite read-bytes value --- ioio/main.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ioio/main.cpp b/ioio/main.cpp index 57e5c9b..3f6c894 100644 --- a/ioio/main.cpp +++ b/ioio/main.cpp @@ -328,8 +328,11 @@ static int get_io_class_id(var_s *map, var_s *retval) { static int cmd_twimaster_readwrite(var_s *self, int argc, slib_par_t *arg, var_s *retval) { int result = 0; + auto readBytes = get_param_int(argc, arg, 1, 0); if (argc < 2) { - error(retval, "TwiMaster.readWrite(address, bytes, [data]", 2, ARRAY_SIZE); + error(retval, "TwiMaster.readWrite(address, read-bytes, [data]", 2, ARRAY_SIZE); + } else if (readBytes < 1 || readBytes > 8) { + error(retval, "read-bytes value out of range. Expected a number between 1 and 8"); } else { int id = get_io_class_id(self, retval); if (id != -1) { From e5c6d665ac4f7bcc2dccf8b8c844c2e82d710e6c Mon Sep 17 00:00:00 2001 From: chrisws Date: Sat, 24 Feb 2024 18:56:17 +1030 Subject: [PATCH 011/131] IOIO: switchover from purejavacomm to fazecast for serial comms --- ioio/pom.xml | 6 +- .../src/main/java/ioio/PausedInputStream.java | 56 ----- .../ioio/lib/pc/SerialPortIOIOConnection.java | 201 +++--------------- .../pc/SerialPortIOIOConnectionBootstrap.java | 87 +++----- 4 files changed, 70 insertions(+), 280 deletions(-) delete mode 100644 ioio/src/main/java/ioio/PausedInputStream.java diff --git a/ioio/pom.xml b/ioio/pom.xml index e81fb20..7f4be41 100644 --- a/ioio/pom.xml +++ b/ioio/pom.xml @@ -17,9 +17,9 @@ 5.07 - com.github.purejavacomm - purejavacomm - 1.0.2.RELEASE + com.fazecast + jSerialComm + 2.10.4 diff --git a/ioio/src/main/java/ioio/PausedInputStream.java b/ioio/src/main/java/ioio/PausedInputStream.java deleted file mode 100644 index f081d9f..0000000 --- a/ioio/src/main/java/ioio/PausedInputStream.java +++ /dev/null @@ -1,56 +0,0 @@ -package ioio; - -import java.io.IOException; -import java.io.InputStream; - -/** - * Pause between read() to avoid excessive CPU usage - */ -public class PausedInputStream extends InputStream { - private long lastAccessMillis; - private final InputStream wrapped; - private boolean closed; - - public PausedInputStream(InputStream inputStream) { - this.wrapped = inputStream; - this.lastAccessMillis = System.currentTimeMillis(); - this.closed = false; - } - - @Override - public void close() throws IOException { - wrapped.close(); - closed = true; - } - - @Override - public synchronized int read(byte[] bytes, int offset, int len) throws IOException { - throw new UnsupportedOperationException(); - } - - @Override - public int read(byte[] bytes) throws IOException { - pause(); - int result = -1; - while (!closed) { - result = wrapped.read(bytes); - if (result > 0) { - break; - } - } - return result; - } - - @Override - public synchronized int read() throws IOException { - throw new UnsupportedOperationException(); - } - - private void pause() throws IOException { - try { - lastAccessMillis = TimerUtil.tick(lastAccessMillis); - } catch (InterruptedException e) { - throw new IOException(e); - } - } -} diff --git a/ioio/src/main/java/ioio/lib/pc/SerialPortIOIOConnection.java b/ioio/src/main/java/ioio/lib/pc/SerialPortIOIOConnection.java index 5c96e0f..dd4d151 100644 --- a/ioio/src/main/java/ioio/lib/pc/SerialPortIOIOConnection.java +++ b/ioio/src/main/java/ioio/lib/pc/SerialPortIOIOConnection.java @@ -1,205 +1,72 @@ -/* - * Copyright 2011 Ytai Ben-Tsvi. All rights reserved. - * - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED - * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND - * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARSHAN POURSOHI OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * The views and conclusions contained in the software and documentation are those of the - * authors and should not be interpreted as representing official policies, either expressed - * or implied. - */ package ioio.lib.pc; -import ioio.PausedInputStream; +import com.fazecast.jSerialComm.SerialPort; import ioio.lib.api.IOIOConnection; import ioio.lib.api.exception.ConnectionLostException; -import ioio.lib.impl.FixedReadBufferedInputStream; import ioio.lib.spi.Log; -import purejavacomm.CommPort; -import purejavacomm.CommPortIdentifier; -import purejavacomm.NoSuchPortException; -import purejavacomm.SerialPort; -import java.io.BufferedOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -class SerialPortIOIOConnection implements IOIOConnection { - private final String name_; - // private static final String TAG = "SerialPortIOIOConnection"; - private boolean abort_ = false; - private SerialPort serialPort_; - private InputStream inputStream_; - private OutputStream outputStream_; - public static final String TAG = "SerialPortIOIOConnection"; - - public SerialPortIOIOConnection(String name) { - name_ = name; +public class SerialPortIOIOConnection implements IOIOConnection { + private static final String TAG = "SerialPortIOIOConnection"; + private static final int READ_TIMEOUT_MILLIS = 20000; + private static final int BAUD_RATE = 115200; + private final SerialPort serialPort; + private InputStream inputStream; + private OutputStream outputStream; + private boolean abort; + + public SerialPortIOIOConnection(String portName) { + serialPort = SerialPort.getCommPort(portName); + serialPort.setBaudRate(BAUD_RATE); + serialPort.setComPortTimeouts(SerialPort.TIMEOUT_READ_BLOCKING | SerialPort.TIMEOUT_WRITE_BLOCKING, READ_TIMEOUT_MILLIS, 0); + abort = false; } @Override public void waitForConnect() throws ConnectionLostException { - while (!abort_) { - try { - CommPortIdentifier identifier = CommPortIdentifier.getPortIdentifier(name_); - CommPort commPort = identifier.open(this.getClass().getName(), 1000); - synchronized (this) { - if (!abort_) { - serialPort_ = (SerialPort) commPort; - serialPort_.enableReceiveThreshold(1); - serialPort_.enableReceiveTimeout(500); - - inputStream_ = new FixedReadBufferedInputStream(new PausedInputStream(serialPort_.getInputStream()), 1024); - outputStream_ = new BufferedOutputStream(serialPort_.getOutputStream(), 256); - - // This is only required on Windows and OSX El Capitan, but otherwise harmless. - //serialPort_.setDTR(false); - serialPort_.setDTR(true); - Thread.sleep(100); - return; - } - } - } catch (NoSuchPortException e) { - Log.d(TAG, e.toString(), e); - try { - Thread.sleep(1000); - } catch (InterruptedException e1) { - Log.d(TAG, e1.toString(), e1); - } - } catch (Exception e) { - Log.d(TAG, e.toString(), e); - if (serialPort_ != null) { - serialPort_.close(); - } - } + if (!abort && serialPort.openPort()) { + inputStream = serialPort.getInputStream(); + outputStream = serialPort.getOutputStream(); + serialPort.setDTR(); + } else { + throw new ConnectionLostException(); } - throw new ConnectionLostException(); } @Override synchronized public void disconnect() { - abort_ = true; - if (serialPort_ != null) { + abort = true; + if (serialPort != null) { try { - inputStream_.close(); + inputStream.close(); } catch (IOException e) { + Log.i(TAG, e.toString()); } - serialPort_.close(); + serialPort.closePort(); } } @Override public InputStream getInputStream() throws ConnectionLostException { - return inputStream_; + if (inputStream == null) { + throw new ConnectionLostException(); + } + return inputStream; } @Override public OutputStream getOutputStream() throws ConnectionLostException { - return outputStream_; + if (outputStream == null) { + throw new ConnectionLostException(); + } + return outputStream; } @Override public boolean canClose() { return true; } - - // This is a hack: - // On Windows, PJC will sometimes block a read until its timeout (which is ideally infinite in - // our case) despite the fact that data is available. - // The workaround is to set a timeout on the InputStream and to read in a loop until something - // is actually read. - // Since a timeout is indistinguishable from an end-of-stream when using the no-argument read(), - // we set a flag to designate that this is a real - // close, prior to actually closing, causing the read loop to exit upon the next timeout. - private static class GracefullyClosingInputStream extends InputStream { - private final InputStream underlying_; - private boolean closed_ = false; - - public GracefullyClosingInputStream(InputStream is) { - underlying_ = is; - } - - @Override - public int read(byte[] b) throws IOException { - while (!closed_) { - int i = underlying_.read(b); - if (i > 0) { - return i; - } - } - return -1; - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - while (!closed_) { - int i = underlying_.read(b, off, len); - if (i > 0) { - return i; - } - } - return -1; - } - - @Override - public long skip(long n) throws IOException { - return underlying_.skip(n); - } - - @Override - public int available() throws IOException { - return underlying_.available(); - } - - @Override - public void close() throws IOException { - closed_ = true; - underlying_.close(); - } - - @Override - public synchronized void mark(int readlimit) { - underlying_.mark(readlimit); - } - - @Override - public synchronized void reset() throws IOException { - underlying_.reset(); - } - - @Override - public boolean markSupported() { - return underlying_.markSupported(); - } - - @Override - public int read() throws IOException { - while (!closed_) { - int i = underlying_.read(); - if (i >= 0) { - return i; - } - } - return -1; - } - } } diff --git a/ioio/src/main/java/ioio/lib/pc/SerialPortIOIOConnectionBootstrap.java b/ioio/src/main/java/ioio/lib/pc/SerialPortIOIOConnectionBootstrap.java index 7d50fb1..026625a 100644 --- a/ioio/src/main/java/ioio/lib/pc/SerialPortIOIOConnectionBootstrap.java +++ b/ioio/src/main/java/ioio/lib/pc/SerialPortIOIOConnectionBootstrap.java @@ -29,84 +29,39 @@ package ioio.lib.pc; +import com.fazecast.jSerialComm.SerialPort; import ioio.lib.api.IOIOConnection; import ioio.lib.spi.IOIOConnectionBootstrap; import ioio.lib.spi.IOIOConnectionFactory; import ioio.lib.spi.Log; -import purejavacomm.CommPort; -import purejavacomm.CommPortIdentifier; -import purejavacomm.PortInUseException; +import java.util.Arrays; import java.util.Collection; -import java.util.Enumeration; +import java.util.Collections; import java.util.LinkedList; import java.util.List; public class SerialPortIOIOConnectionBootstrap implements IOIOConnectionBootstrap { private static final String TAG = "SerialPortIOIOConnectionBootstrap"; - static Collection getAllOpenablePorts() { - List result = new LinkedList<>(); - Enumeration identifiers = CommPortIdentifier.getPortIdentifiers(); - while (identifiers.hasMoreElements()) { - final CommPortIdentifier identifier = identifiers.nextElement(); - if (identifier.getPortType() == CommPortIdentifier.PORT_SERIAL) { - if (checkIdentifier(identifier)) { - Log.d(TAG, "Adding serial port " + identifier.getName()); - result.add(identifier.getName()); - } else { - Log.w(TAG, "Serial port " + identifier.getName() + " cannot be opened. Not adding."); - } - } - } - return result; - } - - static Collection getExplicitPorts() { - String property = System.getProperty("ioio.SerialPorts"); - if (property == null) { - return null; - } - List result = new LinkedList<>(); - String[] portNames = property.split(":"); - for (String portName : portNames) { - result.add(portName); - } - return result; - } - - static boolean checkIdentifier(CommPortIdentifier id) { - if (id.isCurrentlyOwned()) { - return false; - } - // The only way to find out is apparently to try to open the port... - try { - CommPort port = id.open(SerialPortIOIOConnectionBootstrap.class.getName(), 1000); - port.close(); - } catch (PortInUseException e) { - return false; - } - return true; - } - @Override public void getFactories(Collection result) { Collection ports = getExplicitPorts(); - if (ports == null) { + if (ports.isEmpty()) { Log.w(TAG, "ioio.SerialPorts not defined.\n" + "Will attempt to enumerate all possible ports (slow) " + "and connect to a IOIO over each one.\n" + "To fix, add the -Dioio.SerialPorts=xyz argument to " + "the java command line, where xyz is a colon-separated " + "list of port identifiers, e.g. COM1:COM2."); - ports = getAllOpenablePorts(); + ports = getAvailablePorts(); } for (final String port : ports) { Log.d(TAG, "Adding serial port " + port); result.add(new IOIOConnectionFactory() { @Override - public String getType() { - return SerialPortIOIOConnection.class.getCanonicalName(); + public IOIOConnection createConnection() { + return new SerialPortIOIOConnection(port); } @Override @@ -115,10 +70,34 @@ public Object getExtra() { } @Override - public IOIOConnection createConnection() { - return new SerialPortIOIOConnection(port); + public String getType() { + return SerialPortIOIOConnection.class.getCanonicalName(); } }); } } + + static Collection getAvailablePorts() { + List result = new LinkedList<>(); + for (SerialPort port : SerialPort.getCommPorts()) { + if (port.openPort()) { + Log.d(TAG, "Adding serial port " + port.getDescriptivePortName()); + result.add(port.getDescriptivePortName()); + port.closePort(); + } + } + return result; + } + + static Collection getExplicitPorts() { + Collection result; + String property = System.getProperty("ioio.SerialPorts"); + if (property == null) { + result = Collections.emptyList(); + } else { + String[] portNames = property.split(":"); + result = new LinkedList<>(Arrays.asList(portNames)); + } + return result; + } } From 3d41d63134342162264c4a084a01c08b880fa1d7 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Mon, 26 Feb 2024 17:43:29 +1030 Subject: [PATCH 012/131] IOIO: tidy up error messages --- ioio/main.cpp | 17 ++++++++--------- ioio/mkapi.bas | 3 +-- ioio/samples/bme280.bas | 2 +- .../sourceforge/smallbasic/ioio/IOService.java | 4 ++-- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/ioio/main.cpp b/ioio/main.cpp index 3f6c894..9da224c 100644 --- a/ioio/main.cpp +++ b/ioio/main.cpp @@ -51,24 +51,23 @@ struct IOTask { _array = nullptr; } - bool create(const char *path) { + bool create(const char *path, var_s *retval) { bool result; if (_instance != nullptr) { - // error when already constructed + error(retval, "Internal error - already constructed"); result = false; } else { _clazz = env->FindClass(path); - if (_clazz == nullptr) { - env->ExceptionDescribe(); - } else { + if (_clazz != nullptr) { jmethodID constructor = env->GetMethodID(_clazz, "", "()V"); - if (constructor == nullptr) { - env->ExceptionDescribe(); - } else { + if (constructor != nullptr) { _instance = env->NewObject(_clazz, constructor); } } result = _instance != nullptr; + if (!result) { + checkException(retval); + } } return result; } @@ -431,7 +430,7 @@ int sblib_init(const char *sourceFile) { } ioioTask = new IOTask(); - if (!ioioTask || !ioioTask->create(CLASS_IOIO)) { + if (!ioioTask || !ioioTask->create(CLASS_IOIO, nullptr)) { fprintf(stderr, "Failed to IOIOTask\n"); result = 0; } diff --git a/ioio/mkapi.bas b/ioio/mkapi.bas index a6ba571..d874d1e 100644 --- a/ioio/mkapi.bas +++ b/ioio/mkapi.bas @@ -116,14 +116,13 @@ sub generate_open_function(byref obj) endif print " int id = ++nextId;" print " IOTask &instance = _ioTaskMap[id];" - print " if (instance.create(CLASS_" + upper(obj.name) + ") &&" + print " if (instance.create(CLASS_" + upper(obj.name) + ", retval) &&" print " instance." + openFunc + "retval)) {" print " map_init_id(retval, id, CLASS_IOTASK_ID);" print " create_" + lower(obj.name) + "(retval);" print " result = 1;" print " } else {" print " _ioTaskMap.erase(id);" - print " error(retval, \"open" + obj.name + "() failed\");" print " result = 0;" print " }" print " return result;" diff --git a/ioio/samples/bme280.bas b/ioio/samples/bme280.bas index 97e4e0e..e8889c7 100644 --- a/ioio/samples/bme280.bas +++ b/ioio/samples/bme280.bas @@ -80,7 +80,7 @@ func bme280_init(t_mode, p_mode, h_mode, iir) result.H4 = (read8(0xE4) lshift 4) + (a mod 16) result.H5 = (read8(0xE6) lshift 4) + (a rshift 4) result.H6 = read8(0xE7) - if H6 > 127 then result.H6 -= 256 + if result.H6 > 127 then result.H6 -= 256 write8(0xF2, h_mode) sleep_ms(10) diff --git a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOService.java b/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOService.java index 3dc6c83..e87507c 100644 --- a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOService.java +++ b/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOService.java @@ -59,10 +59,10 @@ public void start() { private void registerPin(int pin) throws IOException { if (pin != -1) { if (pin < 0 || pin > MAX_PINS) { - throw new IOException("invalid pin: " + pin); + throw new IOException("Invalid pin: " + pin); } if (usedPins[pin] != null && usedPins[pin]) { - throw new IOException("pin already used: " + pin); + throw new IOException("Pin already used: " + pin); } usedPins[pin] = true; } From 5fc8cfc5e748e862e2cfa5d868d3f39139c2779e Mon Sep 17 00:00:00 2001 From: chrisws Date: Mon, 11 Mar 2024 19:25:40 +1030 Subject: [PATCH 013/131] IOIO: rework build for android --- include/param.cpp | 4 ++ ioio/Makefile.am | 2 +- ioio/README-build.md | 29 ++++++++++++++ ioio/build.gradle | 25 ++++++++++++ ioio/gradle.properties | 21 ++++++++++ ioio/ioio/Android.mk | 22 +++++++++++ ioio/ioio/build.gradle | 39 +++++++++++++++++++ ioio/{ => ioio}/pom.xml | 9 +++++ ioio/ioio/proguard-rules.pro | 8 ++++ .../src/main/java/ioio/lib/RuntimeUtil.java | 23 +++++++++++ .../java/ioio/lib/android/AndroidLogger.java | 14 +++++++ .../ioio/lib/pc/SerialPortIOIOConnection.java | 0 .../pc/SerialPortIOIOConnectionBootstrap.java | 0 .../src/main/java/ioio/lib/spi/LogImpl.java | 21 +++++++++- .../smallbasic/ioio/AnalogInputImpl.java | 0 .../smallbasic/ioio/CapSenseImpl.java | 0 .../smallbasic/ioio/ConnectionController.java | 20 +++++----- .../sourceforge/smallbasic/ioio/Consumer.java | 0 .../smallbasic/ioio/DigitalInputImpl.java | 0 .../smallbasic/ioio/DigitalOutputImpl.java | 0 .../sourceforge/smallbasic/ioio/Function.java | 0 .../sourceforge/smallbasic/ioio/IOIOImpl.java | 1 - .../smallbasic/ioio/IOIOLoader.java | 8 ++++ .../sourceforge/smallbasic/ioio/IOLock.java | 0 .../smallbasic/ioio/IOService.java | 1 - .../sourceforge/smallbasic/ioio/IOTask.java | 0 .../smallbasic/ioio/PulseInputImpl.java | 0 .../smallbasic/ioio/PwmOutputImpl.java | 0 .../smallbasic/ioio/SpiMasterImpl.java | 0 .../smallbasic}/ioio/TimerUtil.java | 2 +- .../smallbasic/ioio/TwiMasterImpl.java | 0 .../smallbasic/ioio/AnalogInputTest.java | 0 .../smallbasic/ioio/DigitalOutputTest.java | 0 ioio/main.cpp | 19 +++++++-- ioio/settings.gradle | 1 + 35 files changed, 250 insertions(+), 19 deletions(-) create mode 100644 ioio/README-build.md create mode 100644 ioio/build.gradle create mode 100644 ioio/gradle.properties create mode 100644 ioio/ioio/Android.mk create mode 100644 ioio/ioio/build.gradle rename ioio/{ => ioio}/pom.xml (81%) create mode 100644 ioio/ioio/proguard-rules.pro create mode 100644 ioio/ioio/src/main/java/ioio/lib/RuntimeUtil.java create mode 100644 ioio/ioio/src/main/java/ioio/lib/android/AndroidLogger.java rename ioio/{ => ioio}/src/main/java/ioio/lib/pc/SerialPortIOIOConnection.java (100%) rename ioio/{ => ioio}/src/main/java/ioio/lib/pc/SerialPortIOIOConnectionBootstrap.java (100%) rename ioio/{ => ioio}/src/main/java/ioio/lib/spi/LogImpl.java (72%) rename ioio/{ => ioio}/src/main/java/net/sourceforge/smallbasic/ioio/AnalogInputImpl.java (100%) rename ioio/{ => ioio}/src/main/java/net/sourceforge/smallbasic/ioio/CapSenseImpl.java (100%) rename ioio/{ => ioio}/src/main/java/net/sourceforge/smallbasic/ioio/ConnectionController.java (58%) rename ioio/{ => ioio}/src/main/java/net/sourceforge/smallbasic/ioio/Consumer.java (100%) rename ioio/{ => ioio}/src/main/java/net/sourceforge/smallbasic/ioio/DigitalInputImpl.java (100%) rename ioio/{ => ioio}/src/main/java/net/sourceforge/smallbasic/ioio/DigitalOutputImpl.java (100%) rename ioio/{ => ioio}/src/main/java/net/sourceforge/smallbasic/ioio/Function.java (100%) rename ioio/{ => ioio}/src/main/java/net/sourceforge/smallbasic/ioio/IOIOImpl.java (98%) create mode 100644 ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOLoader.java rename ioio/{ => ioio}/src/main/java/net/sourceforge/smallbasic/ioio/IOLock.java (100%) rename ioio/{ => ioio}/src/main/java/net/sourceforge/smallbasic/ioio/IOService.java (99%) rename ioio/{ => ioio}/src/main/java/net/sourceforge/smallbasic/ioio/IOTask.java (100%) rename ioio/{ => ioio}/src/main/java/net/sourceforge/smallbasic/ioio/PulseInputImpl.java (100%) rename ioio/{ => ioio}/src/main/java/net/sourceforge/smallbasic/ioio/PwmOutputImpl.java (100%) rename ioio/{ => ioio}/src/main/java/net/sourceforge/smallbasic/ioio/SpiMasterImpl.java (100%) rename ioio/{src/main/java => ioio/src/main/java/net/sourceforge/smallbasic}/ioio/TimerUtil.java (92%) rename ioio/{ => ioio}/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java (100%) rename ioio/{ => ioio}/src/test/java/net/sourceforge/smallbasic/ioio/AnalogInputTest.java (100%) rename ioio/{ => ioio}/src/test/java/net/sourceforge/smallbasic/ioio/DigitalOutputTest.java (100%) create mode 100644 ioio/settings.gradle diff --git a/include/param.cpp b/include/param.cpp index 8f979c2..135e5af 100644 --- a/include/param.cpp +++ b/include/param.cpp @@ -144,6 +144,10 @@ void v_free(var_t *var) { } } +char *v_getstr(var_t *v) { + return (char *)(v->type != V_STR ? "" : v->v.p.ptr); +} + int set_param_int(int argc, slib_par_t *params, int param, int value, var_t *retval) { int result; if (argc < param || !params[param].byref || params[param].var_p->type != V_INT) { diff --git a/ioio/Makefile.am b/ioio/Makefile.am index edbef39..fa55846 100644 --- a/ioio/Makefile.am +++ b/ioio/Makefile.am @@ -19,7 +19,7 @@ all-am: $(generated) CLEANFILES = $(generated) AM_CXXFLAGS=-fno-rtti -std=c++14 -AM_CPPFLAGS = -I../include -Wall @IOIO_CPPFLAGS@ +AM_CPPFLAGS = -DDESKTOP_MODULE -I../include -Wall @IOIO_CPPFLAGS@ lib_LTLIBRARIES = libioio.la libioio_la_SOURCES = ../include/param.cpp ../include/hashmap.cpp ../include/apiexec.cpp main.cpp $(generated) diff --git a/ioio/README-build.md b/ioio/README-build.md new file mode 100644 index 0000000..cc4d72b --- /dev/null +++ b/ioio/README-build.md @@ -0,0 +1,29 @@ +## Building the desktop jar + +``` +cd ioio +mvn clean package +``` + +The resulting jar can be found in `ioio/target` + +## Building the android aar + +``` +./gradlew clean assemble +``` + +Copy the resulting aar files to SmallBASIC project + +``` +cp ioio/build/outputs/aar/* ~/src/SmallBASIC/src/platform/android/app/libs/ +``` + +### To setup the gradlew command + +1. Download an install gradle per gradle instructions +2. Setup the wrapper + +``` +gradle wrapper +``` diff --git a/ioio/build.gradle b/ioio/build.gradle new file mode 100644 index 0000000..88d228e --- /dev/null +++ b/ioio/build.gradle @@ -0,0 +1,25 @@ +buildscript { + // configure the repositories and dependencies for Gradle itself + repositories { + google() + mavenCentral() + maven { url 'https://jitpack.io' } + } + dependencies { + classpath 'com.android.tools.build:gradle:8.3.0' + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'idea' + +tasks.register('clean', Delete) { + delete rootProject.buildDir +} + diff --git a/ioio/gradle.properties b/ioio/gradle.properties new file mode 100644 index 0000000..c03eac4 --- /dev/null +++ b/ioio/gradle.properties @@ -0,0 +1,21 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +android.enableJetifier=true +android.nonFinalResIds=false +android.nonTransitiveRClass=false +android.useAndroidX=true +org.gradle.jvmargs=-Xmx1536m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true diff --git a/ioio/ioio/Android.mk b/ioio/ioio/Android.mk new file mode 100644 index 0000000..9ed5070 --- /dev/null +++ b/ioio/ioio/Android.mk @@ -0,0 +1,22 @@ +# SmallBASIC +# Copyright(C) 2024 Chris Warren-Smith. +# +# This program is distributed under the terms of the GPL v2.0 or later +# Download the GNU Public License (GPL) from www.gnu.org +# + +JNI_PATH := $(call my-dir) + +include $(call all-subdir-makefiles) +LOCAL_PATH := $(JNI_PATH) + +include $(CLEAR_VARS) +LOCAL_MODULE := ioio +LOCAL_CFLAGS := -DHAVE_CONFIG_H=1 -Wno-unknown-pragmas -I../ -I../../ -I../include +LOCAL_SRC_FILES := ../../include/param.cpp \ + ../../include/hashmap.cpp \ + ../../include/apiexec.cpp \ + ../main.cpp +LOCAL_LDLIBS := -llog -landroid +include $(BUILD_SHARED_LIBRARY) + diff --git a/ioio/ioio/build.gradle b/ioio/ioio/build.gradle new file mode 100644 index 0000000..e31b068 --- /dev/null +++ b/ioio/ioio/build.gradle @@ -0,0 +1,39 @@ +plugins { + id 'com.android.library' +} + +android { + sourceSets { + main { + java { + exclude 'ioio/lib/pc/**' + } + } + } + + namespace 'net.sourceforge.smallbasic.ioio' + compileSdk 34 + + defaultConfig { + minSdk 19 + } + + buildTypes { + release { + ndk { + debugSymbolLevel = 'FULL' + } + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + externalNativeBuild { + ndkBuild { + path './Android.mk' + } + } +} + +dependencies { + implementation 'com.github.ytai.ioio:IOIOLibCore:5.07' +} diff --git a/ioio/pom.xml b/ioio/ioio/pom.xml similarity index 81% rename from ioio/pom.xml rename to ioio/ioio/pom.xml index 7f4be41..1ca48a3 100644 --- a/ioio/pom.xml +++ b/ioio/ioio/pom.xml @@ -24,6 +24,15 @@ + + org.apache.maven.plugins + maven-compiler-plugin + + + ioio/lib/android/** + + + maven-assembly-plugin diff --git a/ioio/ioio/proguard-rules.pro b/ioio/ioio/proguard-rules.pro new file mode 100644 index 0000000..331c411 --- /dev/null +++ b/ioio/ioio/proguard-rules.pro @@ -0,0 +1,8 @@ +# +# For more details, see https://developer.android.com/build/shrink-code +# + +-keep public class * { public *; } +-keepclasseswithmembernames class * { native ; } +-printmapping build/outputs/mapping/release/mapping.txt +-keepattributes LineNumberTable,SourceFile diff --git a/ioio/ioio/src/main/java/ioio/lib/RuntimeUtil.java b/ioio/ioio/src/main/java/ioio/lib/RuntimeUtil.java new file mode 100644 index 0000000..6e46a68 --- /dev/null +++ b/ioio/ioio/src/main/java/ioio/lib/RuntimeUtil.java @@ -0,0 +1,23 @@ +package ioio.lib; + +public class RuntimeUtil { + private static final boolean isRunningOnAndroid; + static { + isRunningOnAndroid = getIsRunningOnAndroid(); + } + + public static boolean isRunningOnAndroid() { + return isRunningOnAndroid; + } + + private static boolean getIsRunningOnAndroid() { + boolean result; + try { + Class.forName("android.os.Build"); + result = true; + } catch (ClassNotFoundException e) { + result = false; + } + return result; + } +} diff --git a/ioio/ioio/src/main/java/ioio/lib/android/AndroidLogger.java b/ioio/ioio/src/main/java/ioio/lib/android/AndroidLogger.java new file mode 100644 index 0000000..c8a8b0e --- /dev/null +++ b/ioio/ioio/src/main/java/ioio/lib/android/AndroidLogger.java @@ -0,0 +1,14 @@ +package ioio.lib.android; + +import android.util.Log; + +public class AndroidLogger implements ioio.lib.spi.Log.ILogger { + public AndroidLogger() { + super(); + } + + @Override + public void write(int level, String tag, String message) { + Log.println(level, tag, message); + } +} diff --git a/ioio/src/main/java/ioio/lib/pc/SerialPortIOIOConnection.java b/ioio/ioio/src/main/java/ioio/lib/pc/SerialPortIOIOConnection.java similarity index 100% rename from ioio/src/main/java/ioio/lib/pc/SerialPortIOIOConnection.java rename to ioio/ioio/src/main/java/ioio/lib/pc/SerialPortIOIOConnection.java diff --git a/ioio/src/main/java/ioio/lib/pc/SerialPortIOIOConnectionBootstrap.java b/ioio/ioio/src/main/java/ioio/lib/pc/SerialPortIOIOConnectionBootstrap.java similarity index 100% rename from ioio/src/main/java/ioio/lib/pc/SerialPortIOIOConnectionBootstrap.java rename to ioio/ioio/src/main/java/ioio/lib/pc/SerialPortIOIOConnectionBootstrap.java diff --git a/ioio/src/main/java/ioio/lib/spi/LogImpl.java b/ioio/ioio/src/main/java/ioio/lib/spi/LogImpl.java similarity index 72% rename from ioio/src/main/java/ioio/lib/spi/LogImpl.java rename to ioio/ioio/src/main/java/ioio/lib/spi/LogImpl.java index 5df2988..193d106 100644 --- a/ioio/src/main/java/ioio/lib/spi/LogImpl.java +++ b/ioio/ioio/src/main/java/ioio/lib/spi/LogImpl.java @@ -28,13 +28,30 @@ */ package ioio.lib.spi; +import ioio.lib.RuntimeUtil; import ioio.lib.spi.Log.ILogger; public class LogImpl implements ILogger { private static final char[] LEVELS = {'0', '1', 'V', 'D', 'I', 'W', 'E', 'F'}; + private static final ILogger logger = RuntimeUtil.isRunningOnAndroid() ? getAndroidLogger() : null; @Override - public void write(int priority, String tag, String msg) { - System.err.println("[" + LEVELS[priority] + "/" + tag + "] " + msg); + public void write(int level, String tag, String message) { + if (logger == null) { + System.err.println("[" + LEVELS[level] + "/" + tag + "] " + message); + } else { + logger.write(level, tag, message); + } + } + + private static ILogger getAndroidLogger() { + ILogger result; + try { + result = (ILogger) Class.forName("ioio.lib.android.AndroidLogger").newInstance(); + } + catch (IllegalAccessException | InstantiationException | ClassNotFoundException e) { + result = null; + } + return result; } } diff --git a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/AnalogInputImpl.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/AnalogInputImpl.java similarity index 100% rename from ioio/src/main/java/net/sourceforge/smallbasic/ioio/AnalogInputImpl.java rename to ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/AnalogInputImpl.java diff --git a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/CapSenseImpl.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/CapSenseImpl.java similarity index 100% rename from ioio/src/main/java/net/sourceforge/smallbasic/ioio/CapSenseImpl.java rename to ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/CapSenseImpl.java diff --git a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/ConnectionController.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/ConnectionController.java similarity index 58% rename from ioio/src/main/java/net/sourceforge/smallbasic/ioio/ConnectionController.java rename to ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/ConnectionController.java index c8471b9..d8d6dc6 100644 --- a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/ConnectionController.java +++ b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/ConnectionController.java @@ -1,5 +1,6 @@ package net.sourceforge.smallbasic.ioio; +import ioio.lib.RuntimeUtil; import ioio.lib.util.IOIOBaseApplicationHelper; import ioio.lib.util.IOIOConnectionManager; import ioio.lib.util.IOIOConnectionRegistry; @@ -9,16 +10,17 @@ public class ConnectionController extends IOIOBaseApplicationHelper { private final IOIOConnectionManager manager = new IOIOConnectionManager(this); static { - IOIOConnectionRegistry.addBootstraps(new String[]{ - "ioio.lib.pc.SerialPortIOIOConnectionBootstrap" - }); - /* + if (RuntimeUtil.isRunningOnAndroid()) { + IOIOConnectionRegistry.addBootstraps(new String[]{ + "ioio.lib.impl.SocketIOIOConnectionBootstrap", + "ioio.lib.android.accessory.AccessoryConnectionBootstrap", + "ioio.lib.android.bluetooth.BluetoothIOIOConnectionBootstrap", + "ioio.lib.android.device.DeviceConnectionBootstrap"}); + } else { IOIOConnectionRegistry.addBootstraps(new String[]{ - "ioio.lib.impl.SocketIOIOConnectionBootstrap", - "ioio.lib.android.accessory.AccessoryConnectionBootstrap", - "ioio.lib.android.bluetooth.BluetoothIOIOConnectionBootstrap", - "ioio.lib.android.device.DeviceConnectionBootstrap"}); - */ + "ioio.lib.pc.SerialPortIOIOConnectionBootstrap" + }); + } } public ConnectionController(IOIOLooperProvider provider) { diff --git a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/Consumer.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/Consumer.java similarity index 100% rename from ioio/src/main/java/net/sourceforge/smallbasic/ioio/Consumer.java rename to ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/Consumer.java diff --git a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/DigitalInputImpl.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/DigitalInputImpl.java similarity index 100% rename from ioio/src/main/java/net/sourceforge/smallbasic/ioio/DigitalInputImpl.java rename to ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/DigitalInputImpl.java diff --git a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/DigitalOutputImpl.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/DigitalOutputImpl.java similarity index 100% rename from ioio/src/main/java/net/sourceforge/smallbasic/ioio/DigitalOutputImpl.java rename to ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/DigitalOutputImpl.java diff --git a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/Function.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/Function.java similarity index 100% rename from ioio/src/main/java/net/sourceforge/smallbasic/ioio/Function.java rename to ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/Function.java diff --git a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOImpl.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOImpl.java similarity index 98% rename from ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOImpl.java rename to ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOImpl.java index 4bf5de0..e14df46 100644 --- a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOImpl.java +++ b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOImpl.java @@ -2,7 +2,6 @@ import java.io.IOException; -import ioio.TimerUtil; import ioio.lib.api.IOIO; import ioio.lib.spi.Log; diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOLoader.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOLoader.java new file mode 100644 index 0000000..c4283f8 --- /dev/null +++ b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOLoader.java @@ -0,0 +1,8 @@ +package net.sourceforge.smallbasic.ioio; + +public class IOIOLoader { + public static native void init(); + static { + init(); + } +} diff --git a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOLock.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOLock.java similarity index 100% rename from ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOLock.java rename to ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOLock.java diff --git a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOService.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOService.java similarity index 99% rename from ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOService.java rename to ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOService.java index e87507c..dbb8c38 100644 --- a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOService.java +++ b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOService.java @@ -4,7 +4,6 @@ import java.util.ArrayList; import java.util.List; -import ioio.TimerUtil; import ioio.lib.api.IOIO; import ioio.lib.api.exception.ConnectionLostException; import ioio.lib.spi.Log; diff --git a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOTask.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOTask.java similarity index 100% rename from ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOTask.java rename to ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOTask.java diff --git a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/PulseInputImpl.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/PulseInputImpl.java similarity index 100% rename from ioio/src/main/java/net/sourceforge/smallbasic/ioio/PulseInputImpl.java rename to ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/PulseInputImpl.java diff --git a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/PwmOutputImpl.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/PwmOutputImpl.java similarity index 100% rename from ioio/src/main/java/net/sourceforge/smallbasic/ioio/PwmOutputImpl.java rename to ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/PwmOutputImpl.java diff --git a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/SpiMasterImpl.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/SpiMasterImpl.java similarity index 100% rename from ioio/src/main/java/net/sourceforge/smallbasic/ioio/SpiMasterImpl.java rename to ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/SpiMasterImpl.java diff --git a/ioio/src/main/java/ioio/TimerUtil.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TimerUtil.java similarity index 92% rename from ioio/src/main/java/ioio/TimerUtil.java rename to ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TimerUtil.java index c6e9139..74d77df 100644 --- a/ioio/src/main/java/ioio/TimerUtil.java +++ b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TimerUtil.java @@ -1,4 +1,4 @@ -package ioio; +package net.sourceforge.smallbasic.ioio; public class TimerUtil { private static int latency = 50; diff --git a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java similarity index 100% rename from ioio/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java rename to ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java diff --git a/ioio/src/test/java/net/sourceforge/smallbasic/ioio/AnalogInputTest.java b/ioio/ioio/src/test/java/net/sourceforge/smallbasic/ioio/AnalogInputTest.java similarity index 100% rename from ioio/src/test/java/net/sourceforge/smallbasic/ioio/AnalogInputTest.java rename to ioio/ioio/src/test/java/net/sourceforge/smallbasic/ioio/AnalogInputTest.java diff --git a/ioio/src/test/java/net/sourceforge/smallbasic/ioio/DigitalOutputTest.java b/ioio/ioio/src/test/java/net/sourceforge/smallbasic/ioio/DigitalOutputTest.java similarity index 100% rename from ioio/src/test/java/net/sourceforge/smallbasic/ioio/DigitalOutputTest.java rename to ioio/ioio/src/test/java/net/sourceforge/smallbasic/ioio/DigitalOutputTest.java diff --git a/ioio/main.cpp b/ioio/main.cpp index 9da224c..2ddd85b 100644 --- a/ioio/main.cpp +++ b/ioio/main.cpp @@ -411,9 +411,10 @@ SBLIB_API int sblib_func_count() { } int sblib_init(const char *sourceFile) { +#if defined(DESKTOP_MODULE) JavaVMInitArgs vm_args; JavaVMOption options[3]; - options[0].optionString = (char *)"-Djava.class.path=./target/ioio-1.0-jar-with-dependencies.jar"; + options[0].optionString = (char *)"-Djava.class.path=./android/target/ioio-1.0-jar-with-dependencies.jar"; options[1].optionString = (char *)"-Dioio.SerialPorts=IOIO0"; options[2].optionString = (char *)"-Xrs"; //options[2].optionString = "-Xdebug"; @@ -428,15 +429,23 @@ int sblib_init(const char *sourceFile) { if (!result) { fprintf(stderr, "Failed to create JVM\n"); } - +#else + int result = 1; +#endif ioioTask = new IOTask(); - if (!ioioTask || !ioioTask->create(CLASS_IOIO, nullptr)) { - fprintf(stderr, "Failed to IOIOTask\n"); + var_t retval; + if (!ioioTask || !ioioTask->create(CLASS_IOIO, &retval)) { + fprintf(stderr, "Failed to IOIOTask: %s\n", v_getstr(&retval)); result = 0; } return result; } +extern "C" JNIEXPORT void JNICALL + Java_net_sourceforge_smallbasic_ioio_IOIOLoader_init(JNIEnv *androidEnv, jclass /*clazz*/) { + env = androidEnv; +} + SBLIB_API void sblib_free(int cls_id, int id) { if (id != -1) { switch (cls_id) { @@ -458,11 +467,13 @@ void sblib_close(void) { fprintf(stderr, "IOTask leak detected\n"); _ioTaskMap.clear(); } +#if defined(DESKTOP_MODULE) if (jvm) { jvm->DetachCurrentThread(); } // calling this hangs //jvm->DestroyJavaVM(); +#endif env = nullptr; jvm = nullptr; } diff --git a/ioio/settings.gradle b/ioio/settings.gradle new file mode 100644 index 0000000..f6277a7 --- /dev/null +++ b/ioio/settings.gradle @@ -0,0 +1 @@ +include ':ioio' From 846082da58edd3eb4ac19697dc453557cdc6c076 Mon Sep 17 00:00:00 2001 From: chrisws Date: Tue, 12 Mar 2024 18:42:31 +1030 Subject: [PATCH 014/131] Update dependencies --- ioio/ioio/Android.mk | 2 -- nuklear/Nuklear | 2 +- raylib/README.md | 10 +++++--- raylib/func-def.h | 5 ++-- raylib/func.h | 61 ++++++++++++++++++++++++++------------------ raylib/proc-def.h | 1 + raylib/proc.h | 11 ++++++++ raylib/raygui | 2 +- raylib/raylib | 2 +- websocket/mongoose | 2 +- 10 files changed, 61 insertions(+), 37 deletions(-) diff --git a/ioio/ioio/Android.mk b/ioio/ioio/Android.mk index 9ed5070..0ba0551 100644 --- a/ioio/ioio/Android.mk +++ b/ioio/ioio/Android.mk @@ -6,8 +6,6 @@ # JNI_PATH := $(call my-dir) - -include $(call all-subdir-makefiles) LOCAL_PATH := $(JNI_PATH) include $(CLEAR_VARS) diff --git a/nuklear/Nuklear b/nuklear/Nuklear index 24d6a47..8e5c9f7 160000 --- a/nuklear/Nuklear +++ b/nuklear/Nuklear @@ -1 +1 @@ -Subproject commit 24d6a47d73fc538ecdc67bc1e3e7e98c56e2a4d5 +Subproject commit 8e5c9f7f345c7a7560f4538ffc42e7f1baff0f10 diff --git a/raylib/README.md b/raylib/README.md index 9f4c101..64b9393 100644 --- a/raylib/README.md +++ b/raylib/README.md @@ -4,7 +4,7 @@ raylib is a simple and easy-to-use library to enjoy videogames programming. https://www.raylib.com/ -Implemented APIs (610) +Implemented APIs (612) ---------------- | Name | Description | @@ -41,10 +41,11 @@ Implemented APIs (610) | func ColorContrast(color, contrast) | Get color with contrast correction, contrast values between -1.0f and 1.0f | | func ColorFromHSV(hue, saturation, value) | Get a Color from HSV values, hue [0..360], saturation/value [0..1] | | func ColorFromNormalized(normalized) | Get Color from normalized values [0..1] | +| func ColorIsEqual(col1, col2) | Check if two colors are equal | | func ColorNormalize(color) | Get Color normalized as float [0..1] | | func ColorTint(color, tint) | Get color multiplied with another color | | func ColorToHSV(color) | Get HSV values for a Color, hue [0..360], saturation/value [0..1] | -| func ColorToInt(color) | Get hexadecimal value for a Color | +| func ColorToInt(color) | Get hexadecimal value for a Color (0xRRGGBBAA) | | func CompressData(data, dataSize, compDataSize) | Compress data (DEFLATE algorithm), memory must be MemFree() | | func createPhysicsbodycircle() | n/a | | func createPhysicsbodypolygon() | n/a | @@ -235,7 +236,6 @@ Implemented APIs (610) | func GetMonitorWidth(monitor) | Get specified monitor width (current video mode used by monitor) | | func GetMouseDelta() | Get mouse delta between frames | | func GetMousePosition() | Get mouse position XY | -| func GetMouseRay(mousePosition, camera) | Get a ray trace from mouse position | | func GetMouseWheelMove() | Get mouse wheel movement for X or Y, whichever is larger | | func GetMouseWheelMoveV() | Get mouse wheel movement for both X and Y | | func GetMouseX() | Get mouse position X | @@ -260,6 +260,8 @@ Implemented APIs (610) | func GetRenderWidth() | Get current render width (it considers HiDPI) | | func GetScreenHeight() | Get current screen height | | func GetScreenToWorld2D(position, camera) | Get the world space position for a 2d camera screen space position | +| func GetScreenToWorldRay(position, camera) | Get a ray trace from screen position (i.e mouse) | +| func GetScreenToWorldRayEx(position, camera, width, height) | Get a ray trace from screen position (i.e mouse) in a viewport | | func GetScreenWidth() | Get current screen width | | func GetShaderLocation(shader, uniformName) | Get shader uniform location | | func GetShaderLocationAttrib(shader, attribName) | Get shader attribute location | @@ -276,7 +278,6 @@ Implemented APIs (610) | func GetTouchPosition(index) | Get touch position XY for a touch point index (relative to screen size) | | func GetTouchX() | Get touch position X for touch point 0 (relative to screen size) | | func GetTouchY() | Get touch position Y for touch point 0 (relative to screen size) | -| func GetViewRay(mousePosition, camera, width, height) | Get a ray trace from mouse position in a viewport | | func GetWindowHandle() | Get native window handle | | func GetWindowPosition() | Get window position XY on monitor | | func GetWindowScaleDPI() | Get window scale DPI factor | @@ -496,6 +497,7 @@ Implemented APIs (610) | sub SetConfigFlags(flags) | Setup init configuration flags (view FLAGS) | | sub SetExitKey(key) | Set a custom key to exit program (default is ESC) | | func SetGamepadMappings(mappings) | Set internal gamepad mappings (SDL_GameControllerDB) | +| sub SetGamepadVibration(gamepad, leftMotor, rightMotor) | Set gamepad vibration for both motors | | sub SetGesturesEnabled(flags) | Enable a set of gestures using flags | | sub SetMasterVolume(volume) | Set master volume (listener) | | func setmodeldiffusetexture() | n/a | diff --git a/raylib/func-def.h b/raylib/func-def.h index acffa42..44fe99b 100644 --- a/raylib/func-def.h +++ b/raylib/func-def.h @@ -18,6 +18,7 @@ {2, 2, "COLORCONTRAST", cmd_colorcontrast}, {3, 3, "COLORFROMHSV", cmd_colorfromhsv}, {1, 1, "COLORFROMNORMALIZED", cmd_colorfromnormalized}, + {2, 2, "COLORISEQUAL", cmd_colorisequal}, {1, 1, "COLORNORMALIZE", cmd_colornormalize}, {2, 2, "COLORTINT", cmd_colortint}, {1, 1, "COLORTOHSV", cmd_colortohsv}, @@ -107,7 +108,6 @@ {1, 1, "GETMONITORWIDTH", cmd_getmonitorwidth}, {0, 0, "GETMOUSEDELTA", cmd_getmousedelta}, {0, 0, "GETMOUSEPOSITION", cmd_getmouseposition}, - {2, 2, "GETMOUSERAY", cmd_getmouseray}, {0, 0, "GETMOUSEWHEELMOVE", cmd_getmousewheelmove}, {0, 0, "GETMOUSEWHEELMOVEV", cmd_getmousewheelmovev}, {0, 0, "GETMOUSEX", cmd_getmousex}, @@ -127,6 +127,8 @@ {0, 0, "GETRENDERWIDTH", cmd_getrenderwidth}, {0, 0, "GETSCREENHEIGHT", cmd_getscreenheight}, {2, 2, "GETSCREENTOWORLD2D", cmd_getscreentoworld2d}, + {2, 2, "GETSCREENTOWORLDRAY", cmd_getscreentoworldray}, + {4, 4, "GETSCREENTOWORLDRAYEX", cmd_getscreentoworldrayex}, {0, 0, "GETSCREENWIDTH", cmd_getscreenwidth}, {2, 2, "GETSHADERLOCATION", cmd_getshaderlocation}, {2, 2, "GETSHADERLOCATIONATTRIB", cmd_getshaderlocationattrib}, @@ -143,7 +145,6 @@ {1, 1, "GETTOUCHPOSITION", cmd_gettouchposition}, {0, 0, "GETTOUCHX", cmd_gettouchx}, {0, 0, "GETTOUCHY", cmd_gettouchy}, - {4, 4, "GETVIEWRAY", cmd_getviewray}, {0, 0, "GETWINDOWHANDLE", cmd_getwindowhandle}, {0, 0, "GETWINDOWPOSITION", cmd_getwindowposition}, {0, 0, "GETWINDOWSCALEDPI", cmd_getwindowscaledpi}, diff --git a/raylib/func.h b/raylib/func.h index a67ed58..867e354 100644 --- a/raylib/func.h +++ b/raylib/func.h @@ -234,6 +234,17 @@ static int cmd_colorfromnormalized(int argc, slib_par_t *params, var_t *retval) return 1; } +// +// Check if two colors are equal +// +static int cmd_colorisequal(int argc, slib_par_t *params, var_t *retval) { + auto col1 = get_param_color(argc, params, 0); + auto col2 = get_param_color(argc, params, 1); + auto fnResult = ColorIsEqual(col1, col2); + v_setint(retval, fnResult); + return 1; +} + // // Get Color normalized as float [0..1] // @@ -266,7 +277,7 @@ static int cmd_colortohsv(int argc, slib_par_t *params, var_t *retval) { } // -// Get hexadecimal value for a Color +// Get hexadecimal value for a Color (0xRRGGBBAA) // static int cmd_colortoint(int argc, slib_par_t *params, var_t *retval) { auto color = get_param_color(argc, params, 0); @@ -1288,17 +1299,6 @@ static int cmd_getmouseposition(int argc, slib_par_t *params, var_t *retval) { return 1; } -// -// Get a ray trace from mouse position -// -static int cmd_getmouseray(int argc, slib_par_t *params, var_t *retval) { - auto mousePosition = get_param_vec2(argc, params, 0); - auto camera = get_camera_3d(argc, params, 1); - auto fnResult = GetMouseRay(mousePosition, camera); - v_setray(retval, fnResult); - return 1; -} - // // Get mouse wheel movement for X or Y, whichever is larger // @@ -1517,6 +1517,30 @@ static int cmd_getscreentoworld2d(int argc, slib_par_t *params, var_t *retval) { return 1; } +// +// Get a ray trace from screen position (i.e mouse) +// +static int cmd_getscreentoworldray(int argc, slib_par_t *params, var_t *retval) { + auto position = get_param_vec2(argc, params, 0); + auto camera = get_camera_3d(argc, params, 1); + auto fnResult = GetScreenToWorldRay(position, camera); + v_setray(retval, fnResult); + return 1; +} + +// +// Get a ray trace from screen position (i.e mouse) in a viewport +// +static int cmd_getscreentoworldrayex(int argc, slib_par_t *params, var_t *retval) { + auto position = get_param_vec2(argc, params, 0); + auto camera = get_camera_3d(argc, params, 1); + auto width = get_param_num(argc, params, 2, 0); + auto height = get_param_num(argc, params, 3, 0); + auto fnResult = GetScreenToWorldRayEx(position, camera, width, height); + v_setray(retval, fnResult); + return 1; +} + // // Get current screen width // @@ -1689,19 +1713,6 @@ static int cmd_gettouchy(int argc, slib_par_t *params, var_t *retval) { return 1; } -// -// Get a ray trace from mouse position in a viewport -// -static int cmd_getviewray(int argc, slib_par_t *params, var_t *retval) { - auto mousePosition = get_param_vec2(argc, params, 0); - auto camera = get_camera_3d(argc, params, 1); - auto width = get_param_num(argc, params, 2, 0); - auto height = get_param_num(argc, params, 3, 0); - auto fnResult = GetViewRay(mousePosition, camera, width, height); - v_setray(retval, fnResult); - return 1; -} - // // Get native window handle // diff --git a/raylib/proc-def.h b/raylib/proc-def.h index 8d5b84c..36185b6 100644 --- a/raylib/proc-def.h +++ b/raylib/proc-def.h @@ -179,6 +179,7 @@ {1, 1, "SETCLIPBOARDTEXT", cmd_setclipboardtext}, {1, 1, "SETCONFIGFLAGS", cmd_setconfigflags}, {1, 1, "SETEXITKEY", cmd_setexitkey}, + {3, 3, "SETGAMEPADVIBRATION", cmd_setgamepadvibration}, {1, 1, "SETGESTURESENABLED", cmd_setgesturesenabled}, {1, 1, "SETMASTERVOLUME", cmd_setmastervolume}, {3, 3, "SETMODELMESHMATERIAL", cmd_setmodelmeshmaterial}, diff --git a/raylib/proc.h b/raylib/proc.h index 3869fff..dbe11b3 100644 --- a/raylib/proc.h +++ b/raylib/proc.h @@ -2439,6 +2439,17 @@ static int cmd_setexitkey(int argc, slib_par_t *params, var_t *retval) { return 1; } +// +// Set gamepad vibration for both motors +// +static int cmd_setgamepadvibration(int argc, slib_par_t *params, var_t *retval) { + auto gamepad = get_param_int(argc, params, 0, 0); + auto leftMotor = get_param_num(argc, params, 1, 0); + auto rightMotor = get_param_num(argc, params, 2, 0); + SetGamepadVibration(gamepad, leftMotor, rightMotor); + return 1; +} + // // Enable a set of gestures using flags // diff --git a/raylib/raygui b/raylib/raygui index 7fe39be..623bc61 160000 --- a/raylib/raygui +++ b/raylib/raygui @@ -1 +1 @@ -Subproject commit 7fe39be75af7d166c50afb6e6b9013398d66a7e1 +Subproject commit 623bc61f29839d451061252e1cc93edb013c2f8c diff --git a/raylib/raylib b/raylib/raylib index dd8b561..1fad827 160000 --- a/raylib/raylib +++ b/raylib/raylib @@ -1 +1 @@ -Subproject commit dd8b5613cab0d62a2464be5e3b45e835adedea0c +Subproject commit 1fad8277a37b366841eff1497ca07a7042d4a014 diff --git a/websocket/mongoose b/websocket/mongoose index 1a5ea93..9f498d2 160000 --- a/websocket/mongoose +++ b/websocket/mongoose @@ -1 +1 @@ -Subproject commit 1a5ea930ab73e2b727567026f6b990b1fc416c33 +Subproject commit 9f498d2eea27737bea2f8bce47107d328e881ad0 From 301a24249ebb1ebc5f9c72116040b66dc51a98cd Mon Sep 17 00:00:00 2001 From: chrisws Date: Sat, 16 Mar 2024 21:53:30 +1030 Subject: [PATCH 015/131] IOIO: android integration --- ioio/Makefile.am | 4 +++ ioio/build.gradle | 2 -- ioio/ioio/Android.mk | 2 +- ioio/ioio/build.gradle | 19 ++++++++++++ .../sourceforge/smallbasic/ioio/IOIOImpl.java | 5 +++ .../smallbasic/ioio/IOIOLoader.java | 21 +++++++++++-- .../smallbasic/ioio/IOService.java | 5 +++ ioio/log.h | 31 +++++++++++++++++++ ioio/mkapi.bas | 10 +++--- 9 files changed, 88 insertions(+), 11 deletions(-) create mode 100644 ioio/log.h diff --git a/ioio/Makefile.am b/ioio/Makefile.am index fa55846..0b25c24 100644 --- a/ioio/Makefile.am +++ b/ioio/Makefile.am @@ -29,3 +29,7 @@ $(generated): api.json mkapi.bas mkdoc.bas $(sbasic) mkapi.bas > $@ $(sbasic) mkdoc.bas > README.md @touch main.cpp + + +android: + ./gradlew clean :ioio:assemble && ls -l ioio/build/outputs/aar/ && cp ioio/build/outputs/aar/ioio-* ~/src/SmallBASIC/src/platform/android/app/libs/ diff --git a/ioio/build.gradle b/ioio/build.gradle index 88d228e..9ead8db 100644 --- a/ioio/build.gradle +++ b/ioio/build.gradle @@ -3,7 +3,6 @@ buildscript { repositories { google() mavenCentral() - maven { url 'https://jitpack.io' } } dependencies { classpath 'com.android.tools.build:gradle:8.3.0' @@ -22,4 +21,3 @@ apply plugin: 'idea' tasks.register('clean', Delete) { delete rootProject.buildDir } - diff --git a/ioio/ioio/Android.mk b/ioio/ioio/Android.mk index 0ba0551..5090fcb 100644 --- a/ioio/ioio/Android.mk +++ b/ioio/ioio/Android.mk @@ -10,7 +10,7 @@ LOCAL_PATH := $(JNI_PATH) include $(CLEAR_VARS) LOCAL_MODULE := ioio -LOCAL_CFLAGS := -DHAVE_CONFIG_H=1 -Wno-unknown-pragmas -I../ -I../../ -I../include +LOCAL_CFLAGS := -DHAVE_CONFIG_H=1 -DANDROID_MODULE -Wno-unknown-pragmas -I../ -I../../ -I../include LOCAL_SRC_FILES := ../../include/param.cpp \ ../../include/hashmap.cpp \ ../../include/apiexec.cpp \ diff --git a/ioio/ioio/build.gradle b/ioio/ioio/build.gradle index e31b068..2e61424 100644 --- a/ioio/ioio/build.gradle +++ b/ioio/ioio/build.gradle @@ -11,6 +11,7 @@ android { } } + namespace 'net.sourceforge.smallbasic.ioio' compileSdk 34 @@ -37,3 +38,21 @@ android { dependencies { implementation 'com.github.ytai.ioio:IOIOLibCore:5.07' } + +configurations.implementation.setCanBeResolved(true) + +tasks.register('jarWithDependencies', Jar) { + def libDir = file("${project.buildDir}/../lib") + configurations.implementation.files.each { dependencyJar -> + zipTree(dependencyJar).visit { details -> + if (!details.directory) { + copy { + from details.file + into "${libDir}/${details.path}" + } + } + } + } +} + +assemble.dependsOn jarWithDependencies diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOImpl.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOImpl.java index e14df46..0d56ff8 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOImpl.java +++ b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOImpl.java @@ -19,6 +19,11 @@ public void beginBatch() { lock.invoke(IOIO::beginBatch); } + public void close() { + super.close(); + IOService.getInstance().stop(); + } + public void disconnect() { lock.invoke(IOIO::disconnect); } diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOLoader.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOLoader.java index c4283f8..dbfea13 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOLoader.java +++ b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOLoader.java @@ -1,8 +1,23 @@ package net.sourceforge.smallbasic.ioio; +import ioio.lib.spi.Log; + public class IOIOLoader { - public static native void init(); - static { - init(); + private static final String TAG = "IOIOLoader"; + public static native void init(Class clazz); + + public IOIOLoader() { + super(); + init(getClass()); + } + + public static Class findClass(String className) { + try { + Log.i(TAG, "findClass " + className); + return Class.forName("net.sourceforge.smallbasic.ioio." + className); + } + catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } } } diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOService.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOService.java index dbb8c38..31f8c90 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOService.java +++ b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOService.java @@ -55,6 +55,11 @@ public void start() { connectionController.start(); } + public void stop() { + looper.ioio.disconnect(); + connectionController.stop(); + } + private void registerPin(int pin) throws IOException { if (pin != -1) { if (pin < 0 || pin > MAX_PINS) { diff --git a/ioio/log.h b/ioio/log.h new file mode 100644 index 0000000..7c33928 --- /dev/null +++ b/ioio/log.h @@ -0,0 +1,31 @@ +// This file is part of SmallBASIC +// +// Copyright(C) 2024 Chris Warren-Smith. +// +// This program is distributed under the terms of the GPL v2.0 or later +// Download the GNU Public License (GPL) from www.gnu.org +// + +#pragma once + +#include "config.h" + +#if defined(ANDROID_MODULE) + #include + #if defined(_DEBUG) + #define deviceLog(...) __android_log_print(ANDROID_LOG_ERROR, "smallbasic", __VA_ARGS__) + #else + #define deviceLog(...) __android_log_print(ANDROID_LOG_INFO, "smallbasic", __VA_ARGS__) + #endif +#else + #define deviceLog(...) printf(__VA_ARGS__) +#endif + +#if defined(_DEBUG) + #define trace(...) deviceLog(__VA_ARGS__) +#else + #define trace(...) +#endif + +#define logEntered() trace("%s entered (%s %d)", __FUNCTION__, __FILE__, __LINE__); +#define logLeaving() trace("%s leaving (%s %d)", __FUNCTION__, __FILE__, __LINE__); diff --git a/ioio/mkapi.bas b/ioio/mkapi.bas index d874d1e..b5c86aa 100644 --- a/ioio/mkapi.bas +++ b/ioio/mkapi.bas @@ -57,10 +57,10 @@ sub generate_command(objName, method) local getter, indent if (objName == "IOIO") then - getter = "ioioTask->" + getter = "g_ioioTask->" indent = " " else - getter = "_ioTaskMap.at(id)." + getter = "g_ioTaskMap.at(id)." indent = " " print " int id = get_io_class_id(self, retval);" print " if (id != -1) {" @@ -114,15 +114,15 @@ sub generate_open_function(byref obj) print " int pin" + pin + " = get_param_int(argc, params, " + (pin - 1) + ", -1);" next endif - print " int id = ++nextId;" - print " IOTask &instance = _ioTaskMap[id];" + print " int id = ++g_nextId;" + print " IOTask &instance = g_ioTaskMap[id];" print " if (instance.create(CLASS_" + upper(obj.name) + ", retval) &&" print " instance." + openFunc + "retval)) {" print " map_init_id(retval, id, CLASS_IOTASK_ID);" print " create_" + lower(obj.name) + "(retval);" print " result = 1;" print " } else {" - print " _ioTaskMap.erase(id);" + print " g_ioTaskMap.erase(id);" print " result = 0;" print " }" print " return result;" From 0ae734e5df9de97012880ec0c7a48b825b5b93de Mon Sep 17 00:00:00 2001 From: chrisws Date: Sat, 16 Mar 2024 22:09:40 +1030 Subject: [PATCH 016/131] IOIO: android integration --- ioio/ioio/patch.sh | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100755 ioio/ioio/patch.sh diff --git a/ioio/ioio/patch.sh b/ioio/ioio/patch.sh new file mode 100755 index 0000000..0d0cbdc --- /dev/null +++ b/ioio/ioio/patch.sh @@ -0,0 +1,33 @@ +#/bin/bash + +function patch_aar() { + # Create temporary directory + # Extract AAR contents + # Extract classes.jar + # Copy extra classes + # Repackage classes.jar + # Repackage AAR + # Clean up temporary directory + rm -rf "${TEMP_DIR}" && \ + mkdir -p "${TEMP_DIR}" && \ + unzip "build/outputs/aar/${AAR_FILE}" -d "${TEMP_DIR}" && \ + mkdir -p "${CLASSES_DIR}" && \ + unzip "${CLASSES_JAR}" -d "${CLASSES_DIR}" && \ + rm "${CLASSES_JAR}" && \ + cp -r "${DEPENDENCY_CLASSES_DIR}"/ioio* "${CLASSES_DIR}" && \ + (cd "${CLASSES_DIR}" && jar cvf ../classes.jar .) && \ + rm -rf "${CLASSES_DIR}" && \ + (cd "${TEMP_DIR}" && zip -r "../${AAR_FILE}" *) && \ + rm -rf "${TEMP_DIR}" +} + +TEMP_DIR="patch" +DEPENDENCY_CLASSES_DIR="lib" +CLASSES_DIR="${TEMP_DIR}/classes" +CLASSES_JAR="${TEMP_DIR}/classes.jar" + +AAR_FILE="ioio-debug.aar" +patch_aar + +AAR_FILE="ioio-release.aar" +patch_aar From 7b2a32a67b9b61178e06ef6d602a3363d585b8a5 Mon Sep 17 00:00:00 2001 From: chrisws Date: Sun, 17 Mar 2024 13:25:35 +1030 Subject: [PATCH 017/131] IOIO: fix build for android --- ioio/Makefile.am | 6 ++++-- ioio/build.gradle | 5 +++++ ioio/ioio/build.gradle | 46 ++++++++++++++++++++++++++---------------- ioio/ioio/patch.sh | 33 ------------------------------ 4 files changed, 38 insertions(+), 52 deletions(-) delete mode 100755 ioio/ioio/patch.sh diff --git a/ioio/Makefile.am b/ioio/Makefile.am index 0b25c24..bff2ea9 100644 --- a/ioio/Makefile.am +++ b/ioio/Makefile.am @@ -30,6 +30,8 @@ $(generated): api.json mkapi.bas mkdoc.bas $(sbasic) mkdoc.bas > README.md @touch main.cpp - android: - ./gradlew clean :ioio:assemble && ls -l ioio/build/outputs/aar/ && cp ioio/build/outputs/aar/ioio-* ~/src/SmallBASIC/src/platform/android/app/libs/ + @./gradlew clean mergeJars && \ + (cd ioio/build/libs && zip ../outputs/aar/ioio-debug.aar classes.jar) && \ + (cd ioio/build/libs && zip ../outputs/aar/ioio-release.aar classes.jar) && \ + cp ioio/build/outputs/aar/ioio-* ~/src/SmallBASIC/src/platform/android/app/libs/ diff --git a/ioio/build.gradle b/ioio/build.gradle index 9ead8db..2b1976e 100644 --- a/ioio/build.gradle +++ b/ioio/build.gradle @@ -3,9 +3,12 @@ buildscript { repositories { google() mavenCentral() + gradlePluginPortal() } + dependencies { classpath 'com.android.tools.build:gradle:8.3.0' + classpath "com.github.johnrengelman:shadow:8.1.1" } } @@ -17,7 +20,9 @@ allprojects { } apply plugin: 'idea' +apply plugin: 'com.github.johnrengelman.shadow' tasks.register('clean', Delete) { delete rootProject.buildDir } + diff --git a/ioio/ioio/build.gradle b/ioio/ioio/build.gradle index 2e61424..1decc67 100644 --- a/ioio/ioio/build.gradle +++ b/ioio/ioio/build.gradle @@ -1,5 +1,6 @@ plugins { id 'com.android.library' + id 'com.github.johnrengelman.shadow' } android { @@ -11,7 +12,6 @@ android { } } - namespace 'net.sourceforge.smallbasic.ioio' compileSdk 34 @@ -24,10 +24,11 @@ android { ndk { debugSymbolLevel = 'FULL' } - minifyEnabled true + minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } + externalNativeBuild { ndkBuild { path './Android.mk' @@ -36,23 +37,34 @@ android { } dependencies { - implementation 'com.github.ytai.ioio:IOIOLibCore:5.07' + api 'com.github.ytai.ioio:IOIOLibCore:5.07' + shadow 'com.github.ytai.ioio:IOIOLibCore:5.07' } -configurations.implementation.setCanBeResolved(true) - -tasks.register('jarWithDependencies', Jar) { - def libDir = file("${project.buildDir}/../lib") - configurations.implementation.files.each { dependencyJar -> - zipTree(dependencyJar).visit { details -> - if (!details.directory) { - copy { - from details.file - into "${libDir}/${details.path}" - } - } - } +// +// Builds a jar from the dependencies, then merges this with the intermediate +// classes.jar to build a 'fat' jar. The makefile eventually inserts the +// updated classes.jar into the final .aar files +// not really the correct or ideal approach but it works. +// + +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + +tasks.register('shadowJar', ShadowJar) { + configurations = [project.configurations.shadow] + dependsOn assemble +} + +tasks.register('mergeJars', ShadowJar) { + archiveClassifier.set('') + archiveFileName = 'classes.jar' + dependsOn 'shadowJar' + configurations.runtimeClasspath.each { File file -> + from(zipTree(file.absolutePath)) } } -assemble.dependsOn jarWithDependencies +mergeJars { + from 'build/libs/ioio.jar' + from 'build/intermediates/aar_main_jar/release/syncReleaseLibJars/classes.jar' +} diff --git a/ioio/ioio/patch.sh b/ioio/ioio/patch.sh deleted file mode 100755 index 0d0cbdc..0000000 --- a/ioio/ioio/patch.sh +++ /dev/null @@ -1,33 +0,0 @@ -#/bin/bash - -function patch_aar() { - # Create temporary directory - # Extract AAR contents - # Extract classes.jar - # Copy extra classes - # Repackage classes.jar - # Repackage AAR - # Clean up temporary directory - rm -rf "${TEMP_DIR}" && \ - mkdir -p "${TEMP_DIR}" && \ - unzip "build/outputs/aar/${AAR_FILE}" -d "${TEMP_DIR}" && \ - mkdir -p "${CLASSES_DIR}" && \ - unzip "${CLASSES_JAR}" -d "${CLASSES_DIR}" && \ - rm "${CLASSES_JAR}" && \ - cp -r "${DEPENDENCY_CLASSES_DIR}"/ioio* "${CLASSES_DIR}" && \ - (cd "${CLASSES_DIR}" && jar cvf ../classes.jar .) && \ - rm -rf "${CLASSES_DIR}" && \ - (cd "${TEMP_DIR}" && zip -r "../${AAR_FILE}" *) && \ - rm -rf "${TEMP_DIR}" -} - -TEMP_DIR="patch" -DEPENDENCY_CLASSES_DIR="lib" -CLASSES_DIR="${TEMP_DIR}/classes" -CLASSES_JAR="${TEMP_DIR}/classes.jar" - -AAR_FILE="ioio-debug.aar" -patch_aar - -AAR_FILE="ioio-release.aar" -patch_aar From 5b0deee020a132a9e4ea224ccd525935a2687a61 Mon Sep 17 00:00:00 2001 From: chrisws Date: Mon, 18 Mar 2024 07:40:58 +1030 Subject: [PATCH 018/131] IOIO: android integration --- .../{RuntimeUtil.java => AndroidUtil.java} | 4 +- .../src/main/java/ioio/lib/spi/LogImpl.java | 4 +- .../smallbasic/ioio/ConnectionController.java | 4 +- .../smallbasic/ioio/IOIOLoader.java | 17 +- ioio/main.cpp | 200 ++++++++++++------ 5 files changed, 145 insertions(+), 84 deletions(-) rename ioio/ioio/src/main/java/ioio/lib/{RuntimeUtil.java => AndroidUtil.java} (84%) diff --git a/ioio/ioio/src/main/java/ioio/lib/RuntimeUtil.java b/ioio/ioio/src/main/java/ioio/lib/AndroidUtil.java similarity index 84% rename from ioio/ioio/src/main/java/ioio/lib/RuntimeUtil.java rename to ioio/ioio/src/main/java/ioio/lib/AndroidUtil.java index 6e46a68..154aec7 100644 --- a/ioio/ioio/src/main/java/ioio/lib/RuntimeUtil.java +++ b/ioio/ioio/src/main/java/ioio/lib/AndroidUtil.java @@ -1,12 +1,12 @@ package ioio.lib; -public class RuntimeUtil { +public class AndroidUtil { private static final boolean isRunningOnAndroid; static { isRunningOnAndroid = getIsRunningOnAndroid(); } - public static boolean isRunningOnAndroid() { + public static boolean isAndroid() { return isRunningOnAndroid; } diff --git a/ioio/ioio/src/main/java/ioio/lib/spi/LogImpl.java b/ioio/ioio/src/main/java/ioio/lib/spi/LogImpl.java index 193d106..80728b1 100644 --- a/ioio/ioio/src/main/java/ioio/lib/spi/LogImpl.java +++ b/ioio/ioio/src/main/java/ioio/lib/spi/LogImpl.java @@ -28,12 +28,12 @@ */ package ioio.lib.spi; -import ioio.lib.RuntimeUtil; +import ioio.lib.AndroidUtil; import ioio.lib.spi.Log.ILogger; public class LogImpl implements ILogger { private static final char[] LEVELS = {'0', '1', 'V', 'D', 'I', 'W', 'E', 'F'}; - private static final ILogger logger = RuntimeUtil.isRunningOnAndroid() ? getAndroidLogger() : null; + private static final ILogger logger = AndroidUtil.isAndroid() ? getAndroidLogger() : null; @Override public void write(int level, String tag, String message) { diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/ConnectionController.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/ConnectionController.java index d8d6dc6..c1a6471 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/ConnectionController.java +++ b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/ConnectionController.java @@ -1,6 +1,6 @@ package net.sourceforge.smallbasic.ioio; -import ioio.lib.RuntimeUtil; +import ioio.lib.AndroidUtil; import ioio.lib.util.IOIOBaseApplicationHelper; import ioio.lib.util.IOIOConnectionManager; import ioio.lib.util.IOIOConnectionRegistry; @@ -10,7 +10,7 @@ public class ConnectionController extends IOIOBaseApplicationHelper { private final IOIOConnectionManager manager = new IOIOConnectionManager(this); static { - if (RuntimeUtil.isRunningOnAndroid()) { + if (AndroidUtil.isAndroid()) { IOIOConnectionRegistry.addBootstraps(new String[]{ "ioio.lib.impl.SocketIOIOConnectionBootstrap", "ioio.lib.android.accessory.AccessoryConnectionBootstrap", diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOLoader.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOLoader.java index dbfea13..9b40750 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOLoader.java +++ b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOLoader.java @@ -4,20 +4,11 @@ public class IOIOLoader { private static final String TAG = "IOIOLoader"; - public static native void init(Class clazz); + public static native void init(Long app); - public IOIOLoader() { + public IOIOLoader(Long activity) { super(); - init(getClass()); - } - - public static Class findClass(String className) { - try { - Log.i(TAG, "findClass " + className); - return Class.forName("net.sourceforge.smallbasic.ioio." + className); - } - catch (ClassNotFoundException e) { - throw new RuntimeException(e); - } + Log.d(TAG, "IOIOLoader: " + activity); + init(activity); } } diff --git a/ioio/main.cpp b/ioio/main.cpp index 2ddd85b..a2049af 100644 --- a/ioio/main.cpp +++ b/ioio/main.cpp @@ -6,6 +6,7 @@ // Copyright(C) 2024 Chris Warren-Smith #include "config.h" +#include "log.h" #include "include/var.h" #include "include/module.h" #include "include/param.h" @@ -18,10 +19,19 @@ struct IOTask; -JNIEnv *env; -JavaVM *jvm; -IOTask *ioioTask; -int nextId = 1; +JNIEnv *g_env; +JavaVM *g_jvm; +IOTask *g_ioioTask; +int g_nextId = 1; +jobject g_activity; + +#if defined(ANDROID_MODULE) + #define attachCurrentThread() g_jvm->AttachCurrentThread(&g_env, nullptr) + #define detachCurrentThread() g_jvm->DetachCurrentThread() +#else + #define attachCurrentThread() + #define detachCurrentThread() +#endif #define CLASS_ANALOGINPUT "net/sourceforge/smallbasic/ioio/AnalogInputImpl" #define CLASS_DIGITALINPUT "net/sourceforge/smallbasic/ioio/DigitalInputImpl" @@ -35,6 +45,21 @@ int nextId = 1; #define CLASS_IOTASK_ID 1 #define ARRAY_SIZE 10 +jclass findClass(const char *path) { +#if defined(ANDROID_MODULE) + // calls MainActivity.findClass() to return the loaded jclass for path + jclass clazz = g_env->GetObjectClass(g_activity); + jmethodID methodId = g_env->GetMethodID(clazz, "findClass", "(Ljava/lang/String;)Ljava/lang/Class;"); + jstring className = g_env->NewStringUTF(path); + jclass result = (jclass) g_env->CallObjectMethod(g_activity, methodId, className); + g_env->DeleteLocalRef(className); + g_env->DeleteLocalRef(clazz); + return result; +#else + return g_env->FindClass(path); +#endif +} + struct IOTask { IOTask(): _clazz(nullptr), @@ -44,7 +69,7 @@ struct IOTask { virtual ~IOTask() { if (_array) { - env->DeleteLocalRef(_array); + g_env->DeleteLocalRef(_array); } _clazz = nullptr; _instance = nullptr; @@ -57,34 +82,36 @@ struct IOTask { error(retval, "Internal error - already constructed"); result = false; } else { - _clazz = env->FindClass(path); + attachCurrentThread(); + _clazz = findClass(path); if (_clazz != nullptr) { - jmethodID constructor = env->GetMethodID(_clazz, "", "()V"); + jmethodID constructor = g_env->GetMethodID(_clazz, "", "()V"); if (constructor != nullptr) { - _instance = env->NewObject(_clazz, constructor); + _instance = g_env->NewObject(_clazz, constructor); } } result = _instance != nullptr; if (!result) { checkException(retval); } + detachCurrentThread(); } return result; } bool checkException(var_s *retval) { - auto exc = env->ExceptionOccurred(); + auto exc = g_env->ExceptionOccurred(); if (exc) { if (retval) { - jclass clazz = env->FindClass("java/lang/Object"); - jmethodID methodId = env->GetMethodID(clazz, "toString", "()Ljava/lang/String;"); - jstring jstr = (jstring) env->CallObjectMethod(exc, methodId); - const char *message = env->GetStringUTFChars(jstr, JNI_FALSE); + jclass clazz = g_env->FindClass("java/lang/Object"); + jmethodID methodId = g_env->GetMethodID(clazz, "toString", "()Ljava/lang/String;"); + jstring jstr = (jstring) g_env->CallObjectMethod(exc, methodId); + const char *message = g_env->GetStringUTFChars(jstr, JNI_FALSE); error(retval, message); - env->ReleaseStringUTFChars(jstr, message); + g_env->ReleaseStringUTFChars(jstr, message); } else { - env->ExceptionDescribe(); - env->ExceptionClear(); + g_env->ExceptionDescribe(); + g_env->ExceptionClear(); } } return exc; @@ -94,10 +121,10 @@ struct IOTask { int invokeBoolVoid(const char *name, var_s *retval) { int result = 0; if (_instance != nullptr) { - jmethodID method = env->GetMethodID(_clazz, name, "()Z"); + jmethodID method = g_env->GetMethodID(_clazz, name, "()Z"); int value = 0; if (method != nullptr) { - value = env->CallBooleanMethod(_instance, method); + value = g_env->CallBooleanMethod(_instance, method); } if (!checkException(retval)) { v_setint(retval, value); @@ -111,15 +138,17 @@ struct IOTask { int invokeFloatVoid(const char *name, var_s *retval) { int result = 0; if (_instance != nullptr) { - jmethodID method = env->GetMethodID(_clazz, name, "()F"); + attachCurrentThread(); + jmethodID method = g_env->GetMethodID(_clazz, name, "()F"); var_num_t value = 0; if (method != nullptr) { - value = env->CallFloatMethod(_instance, method); + value = g_env->CallFloatMethod(_instance, method); } if (!checkException(retval)) { v_setreal(retval, value); result = 1; } + detachCurrentThread(); } return result; } @@ -128,15 +157,17 @@ struct IOTask { int invokeIntVoid(const char *name, var_s *retval) { int result = 0; if (_instance != nullptr) { - jmethodID method = env->GetMethodID(_clazz, name, "()I"); + attachCurrentThread(); + jmethodID method = g_env->GetMethodID(_clazz, name, "()I"); int value = 0; if (method != nullptr) { - value = env->CallIntMethod(_instance, method); + value = g_env->CallIntMethod(_instance, method); } if (!checkException(retval)) { v_setint(retval, value); result = 1; } + detachCurrentThread(); } return result; } @@ -145,13 +176,15 @@ struct IOTask { int invokeVoidBool(const char *name, int value, var_s *retval) { int result = 0; if (_instance != nullptr) { - jmethodID method = env->GetMethodID(_clazz, name, "(Z)V"); + attachCurrentThread(); + jmethodID method = g_env->GetMethodID(_clazz, name, "(Z)V"); if (method != nullptr) { - env->CallVoidMethod(_instance, method, value); + g_env->CallVoidMethod(_instance, method, value); } if (!checkException(retval)) { result = 1; } + detachCurrentThread(); } return result; } @@ -160,13 +193,15 @@ struct IOTask { int invokeVoidFloat(const char *name, var_num_t value, var_s *retval) { int result = 0; if (_instance != nullptr) { - jmethodID method = env->GetMethodID(_clazz, name, "()F"); + attachCurrentThread(); + jmethodID method = g_env->GetMethodID(_clazz, name, "()F"); if (method != nullptr) { - env->CallVoidMethod(_instance, method, value); + g_env->CallVoidMethod(_instance, method, value); } if (!checkException(retval)) { result = 1; } + detachCurrentThread(); } return result; } @@ -175,13 +210,15 @@ struct IOTask { int invokeVoidInt(const char *name, int value, var_s *retval) { int result = 0; if (_instance != nullptr) { - jmethodID method = env->GetMethodID(_clazz, name, "(I)V"); + attachCurrentThread(); + jmethodID method = g_env->GetMethodID(_clazz, name, "(I)V"); if (method != nullptr) { - env->CallVoidMethod(_instance, method, value); + g_env->CallVoidMethod(_instance, method, value); } if (!checkException(retval)) { result = 1; } + detachCurrentThread(); } return result; } @@ -190,13 +227,15 @@ struct IOTask { int invokeVoidInt2(const char *name, int value1, int value2, var_s *retval) { int result = 0; if (_instance != nullptr) { - jmethodID method = env->GetMethodID(_clazz, name, "(II)V"); + attachCurrentThread(); + jmethodID method = g_env->GetMethodID(_clazz, name, "(II)V"); if (method != nullptr) { - env->CallVoidMethod(_instance, method, value1, value2); + g_env->CallVoidMethod(_instance, method, value1, value2); } if (!checkException(retval)) { result = 1; } + detachCurrentThread(); } return result; } @@ -205,13 +244,15 @@ struct IOTask { int invokeVoidInt4(const char *name, int value1, int value2, int value3, int value4, var_s *retval) { int result = 0; if (_instance != nullptr) { - jmethodID method = env->GetMethodID(_clazz, name, "(IIII)V"); + attachCurrentThread(); + jmethodID method = g_env->GetMethodID(_clazz, name, "(IIII)V"); if (method != nullptr) { - env->CallVoidMethod(_instance, method, value1, value2, value3, value4); + g_env->CallVoidMethod(_instance, method, value1, value2, value3, value4); } if (!checkException(retval)) { result = 1; } + detachCurrentThread(); } return result; } @@ -220,13 +261,15 @@ struct IOTask { int invokeVoidVoid(const char *name, var_s *retval) { int result = 0; if (_instance != nullptr) { - jmethodID method = env->GetMethodID(_clazz, name, "()V"); + attachCurrentThread(); + jmethodID method = g_env->GetMethodID(_clazz, name, "()V"); if (method != nullptr) { - env->CallVoidMethod(_instance, method); + g_env->CallVoidMethod(_instance, method); } if (!checkException(retval)) { result = 1; } + detachCurrentThread(); } return result; } @@ -235,18 +278,20 @@ struct IOTask { int invokeReadWrite(int argc, slib_par_t *arg, var_s *retval) { int result = 0; if (_instance != nullptr) { - jmethodID method = env->GetMethodID(_clazz, "readWrite", "(II[BI)J"); + attachCurrentThread(); + jmethodID method = g_env->GetMethodID(_clazz, "readWrite", "(II[BI)J"); var_int_t value = 0; if (method != nullptr) { auto address = get_param_int(argc, arg, 0, 0); auto readBytes = get_param_int(argc, arg, 1, 2); populateByteArray(argc, arg, 2); - value = env->CallIntMethod(_instance, method, address, readBytes, _array, argc - 1); + value = g_env->CallIntMethod(_instance, method, address, readBytes, _array, argc - 1); } if (!checkException(retval)) { v_setint(retval, value); result = 1; } + detachCurrentThread(); } return result; } @@ -255,15 +300,17 @@ struct IOTask { int invokeWrite(int argc, slib_par_t *arg, var_s *retval) { int result = 0; if (_instance != nullptr) { - jmethodID method = env->GetMethodID(_clazz, "write", "(I[BI)V"); + attachCurrentThread(); + jmethodID method = g_env->GetMethodID(_clazz, "write", "(I[BI)V"); if (method != nullptr) { auto address = get_param_int(argc, arg, 0, 0); populateByteArray(argc, arg, 1); - env->CallVoidMethod(_instance, method, address, _array, argc - 1); + g_env->CallVoidMethod(_instance, method, address, _array, argc - 1); } if (!checkException(retval)) { result = 1; } + detachCurrentThread(); } return result; } @@ -271,9 +318,9 @@ struct IOTask { // populate the java byte array with the contents of the basic array void populateByteArray(int argc, slib_par_t *params, int offset) { if (!_array) { - _array = env->NewByteArray(ARRAY_SIZE); + _array = g_env->NewByteArray(ARRAY_SIZE); } - jbyte *elements = env->GetByteArrayElements(_array, nullptr); + jbyte *elements = g_env->GetByteArrayElements(_array, nullptr); if ((argc - offset) == 1 && is_param_array(argc, params, offset)) { // argument is an array (assume of ints) var_s *array = params[offset].var_p; @@ -288,7 +335,7 @@ struct IOTask { } } // make the changes available to the java side - env->ReleaseByteArrayElements(_array, elements, 0); + g_env->ReleaseByteArrayElements(_array, elements, 0); } int open(int pin, var_s *retval) { @@ -309,13 +356,13 @@ struct IOTask { jbyteArray _array; }; -robin_hood::unordered_map _ioTaskMap; +robin_hood::unordered_map g_ioTaskMap; static int get_io_class_id(var_s *map, var_s *retval) { int result = -1; if (is_map(map)) { int id = map->v.m.id; - if (id != -1 && _ioTaskMap.find(id) != _ioTaskMap.end()) { + if (id != -1 && g_ioTaskMap.find(id) != g_ioTaskMap.end()) { result = id; } } @@ -335,7 +382,7 @@ static int cmd_twimaster_readwrite(var_s *self, int argc, slib_par_t *arg, var_s } else { int id = get_io_class_id(self, retval); if (id != -1) { - result = _ioTaskMap.at(id).invokeReadWrite(argc, arg, retval); + result = g_ioTaskMap.at(id).invokeReadWrite(argc, arg, retval); } } return result; @@ -348,7 +395,7 @@ static int cmd_twimaster_write(var_s *self, int argc, slib_par_t *arg, var_s *re } else { int id = get_io_class_id(self, retval); if (id != -1) { - result = _ioTaskMap.at(id).invokeWrite(argc, arg, retval); + result = g_ioTaskMap.at(id).invokeWrite(argc, arg, retval); } } return result; @@ -363,7 +410,7 @@ static int cmd_spimaster_write(var_s *self, int argc, slib_par_t *arg, var_s *re if (id != -1) { auto address = get_param_int(argc, arg, 0, 0); auto data = get_param_int(argc, arg, 1, 0); - result = _ioTaskMap.at(id).invokeVoidInt2("write", address, data, retval); + result = g_ioTaskMap.at(id).invokeVoidInt2("write", address, data, retval); } } return result; @@ -424,35 +471,58 @@ int sblib_init(const char *sourceFile) { vm_args.nOptions = 3; vm_args.options = options; vm_args.ignoreUnrecognized = 0; - int result = (JNI_CreateJavaVM(&jvm, (void **)&env, &vm_args) == JNI_OK && - jvm->AttachCurrentThread((void **)&env, nullptr) == JNI_OK); + int result = (JNI_CreateJavaVM(&g_jvm, (void **)&g_env, &vm_args) == JNI_OK && + g_jvm->AttachCurrentThread((void **)&g_env, nullptr) == JNI_OK); if (!result) { fprintf(stderr, "Failed to create JVM\n"); } #else int result = 1; #endif - ioioTask = new IOTask(); + + g_ioioTask = new IOTask(); var_t retval; - if (!ioioTask || !ioioTask->create(CLASS_IOIO, &retval)) { + if (!g_ioioTask || !g_ioioTask->create(CLASS_IOIO, &retval)) { fprintf(stderr, "Failed to IOIOTask: %s\n", v_getstr(&retval)); result = 0; } return result; } -extern "C" JNIEXPORT void JNICALL - Java_net_sourceforge_smallbasic_ioio_IOIOLoader_init(JNIEnv *androidEnv, jclass /*clazz*/) { - env = androidEnv; +#if defined(ANDROID_MODULE) +extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { + logEntered(); + g_jvm = vm; + + jint result; + if (g_jvm->GetEnv((void**)&g_env, JNI_VERSION_1_6) != JNI_OK) { + result = JNI_ERR; + } else { + result = JNI_VERSION_1_6; + } + logLeaving(); + return result; } +extern "C" JNIEXPORT void JNICALL Java_net_sourceforge_smallbasic_ioio_IOIOLoader_init + (JNIEnv *env, jclass clazz, jobject activity) { + logEntered(); + // get the long value from jobject activity + jclass longClass = env->FindClass("java/lang/Long"); + jmethodID longValueMethod = env->GetMethodID(longClass, "longValue", "()J"); + g_activity = (jobject)env->CallLongMethod(activity, longValueMethod); + g_env = env; +} + +#endif + SBLIB_API void sblib_free(int cls_id, int id) { if (id != -1) { switch (cls_id) { case CLASS_IOTASK_ID: - if (id != -1 && _ioTaskMap.find(id) != _ioTaskMap.end()) { - _ioTaskMap.at(id).invokeVoidVoid("close", nullptr); - _ioTaskMap.erase(id); + if (id != -1 && g_ioTaskMap.find(id) != g_ioTaskMap.end()) { + g_ioTaskMap.at(id).invokeVoidVoid("close", nullptr); + g_ioTaskMap.erase(id); } break; } @@ -460,21 +530,21 @@ SBLIB_API void sblib_free(int cls_id, int id) { } void sblib_close(void) { - if (ioioTask) { - delete ioioTask; + if (g_ioioTask) { + g_ioioTask->invokeVoidVoid("close", nullptr); + delete g_ioioTask; } - if (!_ioTaskMap.empty()) { + if (!g_ioTaskMap.empty()) { fprintf(stderr, "IOTask leak detected\n"); - _ioTaskMap.clear(); + g_ioTaskMap.clear(); } #if defined(DESKTOP_MODULE) - if (jvm) { - jvm->DetachCurrentThread(); + if (g_jvm) { + g_jvm->DetachCurrentThread(); } // calling this hangs //jvm->DestroyJavaVM(); + g_env = nullptr; + g_jvm = nullptr; #endif - env = nullptr; - jvm = nullptr; } - From 357fbc8dc6183ff415e540bf4794d25bb5e0ae98 Mon Sep 17 00:00:00 2001 From: chrisws Date: Mon, 18 Mar 2024 09:35:09 +1030 Subject: [PATCH 019/131] IOIO: android integration --- .../smallbasic/ioio/IOService.java | 4 +- ioio/main.cpp | 67 ++++++++++++++++--- 2 files changed, 59 insertions(+), 12 deletions(-) diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOService.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOService.java index 31f8c90..7734df3 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOService.java +++ b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOService.java @@ -56,7 +56,9 @@ public void start() { } public void stop() { - looper.ioio.disconnect(); + if (looper.ioio != null) { + looper.ioio.disconnect(); + } connectionController.stop(); } diff --git a/ioio/main.cpp b/ioio/main.cpp index a2049af..889bc43 100644 --- a/ioio/main.cpp +++ b/ioio/main.cpp @@ -45,21 +45,49 @@ jobject g_activity; #define CLASS_IOTASK_ID 1 #define ARRAY_SIZE 10 -jclass findClass(const char *path) { #if defined(ANDROID_MODULE) - // calls MainActivity.findClass() to return the loaded jclass for path +// +// calls MainActivity.findClass() to return the loaded jclass for path +// +jclass findClass(const char *path) { jclass clazz = g_env->GetObjectClass(g_activity); jmethodID methodId = g_env->GetMethodID(clazz, "findClass", "(Ljava/lang/String;)Ljava/lang/Class;"); jstring className = g_env->NewStringUTF(path); - jclass result = (jclass) g_env->CallObjectMethod(g_activity, methodId, className); + jclass result = (jclass)g_env->CallObjectMethod(g_activity, methodId, className); g_env->DeleteLocalRef(className); g_env->DeleteLocalRef(clazz); + return reinterpret_cast(g_env->NewGlobalRef(result)); +} + +jobject createInstance(jclass clazz) { + jmethodID constructor = g_env->GetMethodID(clazz, "", "()V"); + jobject result; + if (constructor != nullptr) { + result = g_env->NewObject(clazz, constructor); + result = reinterpret_cast(g_env->NewGlobalRef(result)); + } else { + result = nullptr; + } return result; +} + #else +jclass findClass(const char *path) { return g_env->FindClass(path); -#endif } +jobject createInstance() { + jmethodID constructor = g_env->GetMethodID(clazz, "", "()V"); + jobject result; + if (constructor != nullptr) { + result = g_env->NewObject(clazz, constructor); + } else { + result = nullptr; + } + return result; +} +#endif + struct IOTask { IOTask(): _clazz(nullptr), @@ -68,9 +96,15 @@ struct IOTask { } virtual ~IOTask() { + attachCurrentThread(); if (_array) { g_env->DeleteLocalRef(_array); } +#if defined(ANDROID_MODULE) + g_env->DeleteGlobalRef(_clazz); + g_env->DeleteGlobalRef(_instance); +#endif + detachCurrentThread(); _clazz = nullptr; _instance = nullptr; _array = nullptr; @@ -85,10 +119,7 @@ struct IOTask { attachCurrentThread(); _clazz = findClass(path); if (_clazz != nullptr) { - jmethodID constructor = g_env->GetMethodID(_clazz, "", "()V"); - if (constructor != nullptr) { - _instance = g_env->NewObject(_clazz, constructor); - } + _instance = createInstance(_clazz); } result = _instance != nullptr; if (!result) { @@ -457,6 +488,9 @@ SBLIB_API int sblib_func_count() { return (sizeof(lib_func) / sizeof(lib_func[0])); } +// +// Program startup +// int sblib_init(const char *sourceFile) { #if defined(DESKTOP_MODULE) JavaVMInitArgs vm_args; @@ -490,12 +524,15 @@ int sblib_init(const char *sourceFile) { } #if defined(ANDROID_MODULE) -extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { +// +// Stores the Android JavaVM reference +// +extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void* reserved) { logEntered(); g_jvm = vm; jint result; - if (g_jvm->GetEnv((void**)&g_env, JNI_VERSION_1_6) != JNI_OK) { + if (g_jvm->GetEnv((void **)&g_env, JNI_VERSION_1_6) != JNI_OK) { result = JNI_ERR; } else { result = JNI_VERSION_1_6; @@ -504,10 +541,12 @@ extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { return result; } +// +// Retrieves the _app->activity->clazz value sent from App/JNI to Java to IOIOLoader +// extern "C" JNIEXPORT void JNICALL Java_net_sourceforge_smallbasic_ioio_IOIOLoader_init (JNIEnv *env, jclass clazz, jobject activity) { logEntered(); - // get the long value from jobject activity jclass longClass = env->FindClass("java/lang/Long"); jmethodID longValueMethod = env->GetMethodID(longClass, "longValue", "()J"); g_activity = (jobject)env->CallLongMethod(activity, longValueMethod); @@ -516,6 +555,9 @@ extern "C" JNIEXPORT void JNICALL Java_net_sourceforge_smallbasic_ioio_IOIOLoade #endif +// +// Release ioio variables falling out of scope +// SBLIB_API void sblib_free(int cls_id, int id) { if (id != -1) { switch (cls_id) { @@ -529,6 +571,9 @@ SBLIB_API void sblib_free(int cls_id, int id) { } } +// +// Program termination +// void sblib_close(void) { if (g_ioioTask) { g_ioioTask->invokeVoidVoid("close", nullptr); From fbaf5678a413be547228c146b79bd62a40f6883a Mon Sep 17 00:00:00 2001 From: chrisws Date: Thu, 21 Mar 2024 18:11:50 +1030 Subject: [PATCH 020/131] IOIO: updates for android module --- .../ioio/lib/{ => android}/AndroidUtil.java | 2 +- .../AccessoryConnectionBootstrap.java | 332 ++++++++++++++++++ .../ioio/lib/android/accessory/Adapter.java | 132 +++++++ .../src/main/java/ioio/lib/spi/LogImpl.java | 2 +- .../smallbasic/ioio/AnalogInputImpl.java | 6 +- .../smallbasic/ioio/CapSenseImpl.java | 6 +- .../smallbasic/ioio/ConnectionController.java | 7 +- .../smallbasic/ioio/DigitalInputImpl.java | 6 +- .../smallbasic/ioio/DigitalOutputImpl.java | 6 +- .../smallbasic/ioio/PulseInputImpl.java | 6 +- .../smallbasic/ioio/PwmOutputImpl.java | 6 +- .../smallbasic/ioio/SpiMasterImpl.java | 5 +- .../smallbasic/ioio/TwiMasterImpl.java | 6 +- 13 files changed, 500 insertions(+), 22 deletions(-) rename ioio/ioio/src/main/java/ioio/lib/{ => android}/AndroidUtil.java (94%) create mode 100644 ioio/ioio/src/main/java/ioio/lib/android/accessory/AccessoryConnectionBootstrap.java create mode 100644 ioio/ioio/src/main/java/ioio/lib/android/accessory/Adapter.java diff --git a/ioio/ioio/src/main/java/ioio/lib/AndroidUtil.java b/ioio/ioio/src/main/java/ioio/lib/android/AndroidUtil.java similarity index 94% rename from ioio/ioio/src/main/java/ioio/lib/AndroidUtil.java rename to ioio/ioio/src/main/java/ioio/lib/android/AndroidUtil.java index 154aec7..4ceeb7d 100644 --- a/ioio/ioio/src/main/java/ioio/lib/AndroidUtil.java +++ b/ioio/ioio/src/main/java/ioio/lib/android/AndroidUtil.java @@ -1,4 +1,4 @@ -package ioio.lib; +package ioio.lib.android; public class AndroidUtil { private static final boolean isRunningOnAndroid; diff --git a/ioio/ioio/src/main/java/ioio/lib/android/accessory/AccessoryConnectionBootstrap.java b/ioio/ioio/src/main/java/ioio/lib/android/accessory/AccessoryConnectionBootstrap.java new file mode 100644 index 0000000..7b20634 --- /dev/null +++ b/ioio/ioio/src/main/java/ioio/lib/android/accessory/AccessoryConnectionBootstrap.java @@ -0,0 +1,332 @@ +/* + * Copyright 2015 Ytai Ben-Tsvi. All rights reserved. + * + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARSHAN POURSOHI OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied. + */ + +package ioio.lib.android.accessory; + +import ioio.lib.android.accessory.Adapter.UsbAccessoryInterface; +import ioio.lib.api.IOIOConnection; +import ioio.lib.api.exception.ConnectionLostException; +import ioio.lib.impl.FixedReadBufferedInputStream; +import ioio.lib.spi.IOIOConnectionBootstrap; +import ioio.lib.spi.IOIOConnectionFactory; +import ioio.lib.spi.NoRuntimeSupportException; + +import java.io.BufferedOutputStream; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Collection; + +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +public class AccessoryConnectionBootstrap extends BroadcastReceiver implements IOIOConnectionBootstrap, IOIOConnectionFactory { + private static final String TAG = AccessoryConnectionBootstrap.class.getSimpleName(); + private static final String ACTION_USB_PERMISSION = "ioio.lib.accessory.action.USB_PERMISSION"; + + private ContextWrapper activity; + private final Adapter adapter; + private Adapter.AbstractUsbManager usbManager; + private boolean shouldTryOpen = false; + private PendingIntent pendingIntent; + private ParcelFileDescriptor fileDescriptor; + private InputStream inputStream; + private OutputStream outputStream; + + public AccessoryConnectionBootstrap() throws NoRuntimeSupportException { + adapter = new Adapter(); + } + + //@Override + public void onCreate(ContextWrapper wrapper) { + activity = wrapper; + usbManager = adapter.getManager(wrapper); + registerReceiver(); + } + + //@Override + public void onDestroy() { + unregisterReceiver(); + } + + @Override + public synchronized void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (ACTION_USB_PERMISSION.equals(action)) { + pendingIntent = null; + if (intent.getBooleanExtra(usbManager.EXTRA_PERMISSION_GRANTED, false)) { + notifyAll(); + } else { + Log.e(TAG, "Permission denied"); + } + } + } + + //@Override + public synchronized void open() { + notifyAll(); + } + + //@Override + public synchronized void reopen() { + notifyAll(); + } + + //@Override + public synchronized void close() { + } + + private synchronized void disconnect() { + // This should abort any current open attempt. + shouldTryOpen = false; + notifyAll(); + + // And this should kill any ongoing connections. + if (fileDescriptor != null) { + try { + fileDescriptor.close(); + } catch (IOException e) { + Log.e(TAG, "Failed to close file descriptor.", e); + } + fileDescriptor = null; + } + + if (pendingIntent != null) { + pendingIntent.cancel(); + pendingIntent = null; + } + } + + @Override + public IOIOConnection createConnection() { + return new Connection(); + } + + @Override + public void getFactories(Collection result) { + result.add(this); + } + + @Override + public String getType() { + return Connection.class.getCanonicalName(); + } + + @Override + public Object getExtra() { + return null; + } + + private synchronized void waitForConnect(Connection connection) throws ConnectionLostException { + // In order to simplify the connection process in face of many different sequences of events + // that might occur, we collapsed the entire sequence into one non-blocking method, + // tryOpen(), which tries the entire process from the beginning, undoes everything if + // something along the way fails and always returns immediately. + // This method, simply calls tryOpen() in a loop until it succeeds or until we're no longer + // interested. Between attempts, it waits until "something interesting" has happened, which + // may be permission granted, the client telling us to try again (via reopen()) or stop + // trying, etc. + shouldTryOpen = true; + while (shouldTryOpen) { + if (tryOpen()) { + // Success! + return; + } + forceWait(); + } + throw new ConnectionLostException(); + } + + private void forceWait() { + try { + wait(); + } catch (InterruptedException e) { + Log.e(TAG, "Do not interrupt me!"); + } + } + + private boolean tryOpen() { + // Find the accessory. + UsbAccessoryInterface[] accessories = usbManager.getAccessoryList(); + UsbAccessoryInterface accessory = (accessories == null ? null : accessories[0]); + + if (accessory == null) { + Log.v(TAG, "No accessory found."); + return false; + } + + // Check for permission to access the accessory. + if (!usbManager.hasPermission(accessory)) { + if (pendingIntent == null) { + Log.v(TAG, "Requesting permission."); + pendingIntent = PendingIntent.getBroadcast(activity, 0, new Intent( + ACTION_USB_PERMISSION), 0); + usbManager.requestPermission(accessory, pendingIntent); + } + return false; + } + + boolean success = false; + + // From this point on, if anything goes wrong, we're responsible for canceling the intent. + try { + // Obtain a file descriptor. + fileDescriptor = usbManager.openAccessory(accessory); + if (fileDescriptor == null) { + Log.v(TAG, "Failed to open file descriptor."); + return false; + } + + // From this point on, if anything goes wrong, we're responsible for closing the file + // descriptor. + try { + FileDescriptor fd = fileDescriptor.getFileDescriptor(); + // Apparently, some Android devices (e.g. Nexus 5) only support read operations of + // multiples of the endpoint buffer size. So there you have it! + inputStream = new FixedReadBufferedInputStream(new FileInputStream(fd), 1024); + outputStream = new BufferedOutputStream(new FileOutputStream(fd), 1024); + + // Soft-open the connection + outputStream.write(0x00); + outputStream.flush(); + + // We're going to block now. We're counting on the IOIO to + // write back a byte, or otherwise we're locked until + // physical disconnection. This is a known OpenAccessory + // bug: + // http://code.google.com/p/android/issues/detail?id=20545 + while (inputStream.read() != 1) { + trySleep(1000); + } + + success = true; + return true; + } catch (IOException e) { + Log.v(TAG, "Failed to open streams", e); + return false; + } finally { + if (!success) { + try { + fileDescriptor.close(); + } catch (IOException e) { + Log.e(TAG, "Failed to close file descriptor.", e); + } + fileDescriptor = null; + } + } + } finally { + if (!success && pendingIntent != null) { + pendingIntent.cancel(); + pendingIntent = null; + } + } + } + + private void registerReceiver() { + IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION); + activity.registerReceiver(this, filter); + } + + private void unregisterReceiver() { + activity.unregisterReceiver(this); + } + + private void trySleep(long time) { + synchronized (AccessoryConnectionBootstrap.this) { + try { + AccessoryConnectionBootstrap.this.wait(time); + } catch (InterruptedException e) { + } + } + } + + private static enum InstanceState { + INIT, CONNECTED, DEAD + }; + + private class Connection implements IOIOConnection { + private InstanceState instanceState_ = InstanceState.INIT; + + @Override + public InputStream getInputStream() throws ConnectionLostException { + return inputStream; + } + + @Override + public OutputStream getOutputStream() throws ConnectionLostException { + return outputStream; + } + + @Override + public boolean canClose() { + return false; + } + + @Override + public void waitForConnect() throws ConnectionLostException { + synchronized(AccessoryConnectionBootstrap.this) { + if (instanceState_ != InstanceState.INIT) { + throw new IllegalStateException("waitForConnect() may only be called once"); + } + + try { + AccessoryConnectionBootstrap.this.waitForConnect(this); + instanceState_ = InstanceState.CONNECTED; + } catch (ConnectionLostException e) { + instanceState_ = InstanceState.DEAD; + throw e; + } + } + } + + @Override + public void disconnect() { + synchronized(AccessoryConnectionBootstrap.this) { + if (instanceState_ != InstanceState.DEAD) { + AccessoryConnectionBootstrap.this.disconnect(); + instanceState_ = InstanceState.DEAD; + } + } + } + + @Override + protected void finalize() throws Throwable { + disconnect(); + } + } +} diff --git a/ioio/ioio/src/main/java/ioio/lib/android/accessory/Adapter.java b/ioio/ioio/src/main/java/ioio/lib/android/accessory/Adapter.java new file mode 100644 index 0000000..69c89c5 --- /dev/null +++ b/ioio/ioio/src/main/java/ioio/lib/android/accessory/Adapter.java @@ -0,0 +1,132 @@ +/* + * Copyright 2014 Ytai Ben-Tsvi. All rights reserved. + * + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARSHAN POURSOHI OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied. + */ + +package ioio.lib.android.accessory; + +import ioio.lib.spi.NoRuntimeSupportException; +import android.app.PendingIntent; +import android.content.Context; +import android.content.ContextWrapper; + +import android.hardware.usb.UsbAccessory; +import android.hardware.usb.UsbManager; +import android.os.ParcelFileDescriptor; + +/** + * This class serves as a common interface for USB accessory for either com.android.future.usb or + * + * When this class is instantiated, it will throw a {@link NoRuntimeSupportException} if none of the + * underlying implementations exist. Otherwise, the client may call + * {@link #getManager(ContextWrapper)}, which returns an interface that is equivalent to UsbManager + * on either API. Likewise, the {@link UsbAccessoryInterface} interface is a wrapper around + * UsbAccessory. + * + */ +class Adapter { + Adapter() throws NoRuntimeSupportException { + try { + Class.forName("UsbManager"); + } catch (ClassNotFoundException e) { + throw new NoRuntimeSupportException("No support for USB accesory mode."); + } + } + + AbstractUsbManager getManager(ContextWrapper wrapper) { + return getManagerNew(wrapper); + } + + private AbstractUsbManager getManagerNew(ContextWrapper wrapper) { + final UsbManager manager = (UsbManager) wrapper.getSystemService(Context.USB_SERVICE); + return new NewUsbManager(manager); + } + + static abstract class AbstractUsbManager { + final String EXTRA_PERMISSION_GRANTED; + protected AbstractUsbManager(String action_usb_accessory_detached, + String extra_permission_granted) { + EXTRA_PERMISSION_GRANTED = extra_permission_granted; + } + + abstract UsbAccessoryInterface[] getAccessoryList(); + abstract boolean hasPermission(UsbAccessoryInterface accessory); + abstract ParcelFileDescriptor openAccessory(UsbAccessoryInterface accessory); + abstract void requestPermission(UsbAccessoryInterface accessory, PendingIntent pendingIntent); + } + + private static final class NewUsbManager extends AbstractUsbManager { + private final UsbManager manager; + + private NewUsbManager(UsbManager manager) { + super(UsbManager.ACTION_USB_ACCESSORY_DETACHED, + UsbManager.EXTRA_PERMISSION_GRANTED); + this.manager = manager; + } + + @Override + UsbAccessoryInterface[] getAccessoryList() { + final UsbAccessory[] accessoryList = manager.getAccessoryList(); + if (accessoryList == null) { + return null; + } + UsbAccessoryInterface[] result = new UsbAccessoryInterface[accessoryList.length]; + for (int i = 0; i < accessoryList.length; ++i) { + result[i] = new UsbAccessoryAdapter<>(accessoryList[i]); + } + return result; + } + + @SuppressWarnings("unchecked") + @Override + boolean hasPermission(UsbAccessoryInterface accessory) { + return manager.hasPermission(((UsbAccessoryAdapter) accessory).impl); + } + + @SuppressWarnings("unchecked") + @Override + ParcelFileDescriptor openAccessory(UsbAccessoryInterface accessory) { + return manager.openAccessory(((UsbAccessoryAdapter) accessory).impl); + } + + @SuppressWarnings("unchecked") + @Override + void requestPermission(UsbAccessoryInterface accessory, PendingIntent pendingIntent) { + manager.requestPermission(((UsbAccessoryAdapter) accessory).impl, pendingIntent); + } + } + + private static class UsbAccessoryAdapter implements UsbAccessoryInterface { + final T impl; + UsbAccessoryAdapter(T t) { + impl = t; + } + } + + static interface UsbAccessoryInterface { + } +} diff --git a/ioio/ioio/src/main/java/ioio/lib/spi/LogImpl.java b/ioio/ioio/src/main/java/ioio/lib/spi/LogImpl.java index 80728b1..4f38a43 100644 --- a/ioio/ioio/src/main/java/ioio/lib/spi/LogImpl.java +++ b/ioio/ioio/src/main/java/ioio/lib/spi/LogImpl.java @@ -28,7 +28,7 @@ */ package ioio.lib.spi; -import ioio.lib.AndroidUtil; +import ioio.lib.android.AndroidUtil; import ioio.lib.spi.Log.ILogger; public class LogImpl implements ILogger { diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/AnalogInputImpl.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/AnalogInputImpl.java index a06817b..a80c148 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/AnalogInputImpl.java +++ b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/AnalogInputImpl.java @@ -23,8 +23,10 @@ public int available() { @Override public void close() { super.close(); - input.close(); - input = null; + if (input != null) { + input.close(); + input = null; + } } @Override diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/CapSenseImpl.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/CapSenseImpl.java index bf8a773..c33513c 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/CapSenseImpl.java +++ b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/CapSenseImpl.java @@ -18,8 +18,10 @@ public CapSenseImpl() { @Override public void close() { super.close(); - capSense.close(); - capSense = null; + if (capSense != null) { + capSense.close(); + capSense = null; + } } @Override diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/ConnectionController.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/ConnectionController.java index c1a6471..439f471 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/ConnectionController.java +++ b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/ConnectionController.java @@ -1,6 +1,6 @@ package net.sourceforge.smallbasic.ioio; -import ioio.lib.AndroidUtil; +import ioio.lib.android.AndroidUtil; import ioio.lib.util.IOIOBaseApplicationHelper; import ioio.lib.util.IOIOConnectionManager; import ioio.lib.util.IOIOConnectionRegistry; @@ -12,10 +12,7 @@ public class ConnectionController extends IOIOBaseApplicationHelper { static { if (AndroidUtil.isAndroid()) { IOIOConnectionRegistry.addBootstraps(new String[]{ - "ioio.lib.impl.SocketIOIOConnectionBootstrap", - "ioio.lib.android.accessory.AccessoryConnectionBootstrap", - "ioio.lib.android.bluetooth.BluetoothIOIOConnectionBootstrap", - "ioio.lib.android.device.DeviceConnectionBootstrap"}); + "ioio.lib.android.accessory.AccessoryConnectionBootstrap"}); } else { IOIOConnectionRegistry.addBootstraps(new String[]{ "ioio.lib.pc.SerialPortIOIOConnectionBootstrap" diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/DigitalInputImpl.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/DigitalInputImpl.java index d929d86..643419d 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/DigitalInputImpl.java +++ b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/DigitalInputImpl.java @@ -20,8 +20,10 @@ public DigitalInputImpl() { @Override public void close() { super.close(); - input.close(); - input = null; + if (input != null) { + input.close(); + input = null; + } } @Override diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/DigitalOutputImpl.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/DigitalOutputImpl.java index d6e81b3..75c3ca8 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/DigitalOutputImpl.java +++ b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/DigitalOutputImpl.java @@ -18,8 +18,10 @@ public DigitalOutputImpl() { @Override public void close() { super.close(); - output.close(); - output = null; + if (output != null) { + output.close(); + output = null; + } } @Override diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/PulseInputImpl.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/PulseInputImpl.java index 44a75e3..f106afa 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/PulseInputImpl.java +++ b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/PulseInputImpl.java @@ -19,8 +19,10 @@ public PulseInputImpl() { @Override public void close() { super.close(); - input.close(); - input = null; + if (input != null) { + input.close(); + input = null; + } } @Override diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/PwmOutputImpl.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/PwmOutputImpl.java index 9303f78..e66953f 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/PwmOutputImpl.java +++ b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/PwmOutputImpl.java @@ -24,8 +24,10 @@ public PwmOutputImpl() { @Override public void close() { super.close(); - output.close(); - output = null; + if (output != null) { + output.close(); + output = null; + } } @Override diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/SpiMasterImpl.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/SpiMasterImpl.java index bcd39d2..88ccc01 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/SpiMasterImpl.java +++ b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/SpiMasterImpl.java @@ -24,7 +24,10 @@ public SpiMasterImpl() { @Override public void close() { super.close(); - spiMaster.close(); + if (spiMaster != null) { + spiMaster.close(); + spiMaster = null; + } } public void open(int miso, int mosi, int clk, int slaveSelect) throws IOException { diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java index b736c67..cc6ee1a 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java +++ b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java @@ -23,8 +23,10 @@ public TwiMasterImpl() { @Override public void close() { super.close(); - twiMaster.close(); - twiMaster = null; + if (twiMaster != null) { + twiMaster.close(); + twiMaster = null; + } } public void open(int twiNum, int smbus) throws IOException { From 52a3d19170c6390921edd789c4cda359f2a4ecf0 Mon Sep 17 00:00:00 2001 From: chrisws Date: Thu, 21 Mar 2024 18:12:01 +1030 Subject: [PATCH 021/131] Update dependencies --- nuklear/Makefile.am | 2 +- nuklear/Nuklear | 2 +- raylib/raygui | 2 +- raylib/raylib | 2 +- websocket/mongoose | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/nuklear/Makefile.am b/nuklear/Makefile.am index 43f107c..c924e99 100644 --- a/nuklear/Makefile.am +++ b/nuklear/Makefile.am @@ -6,7 +6,7 @@ # AM_CXXFLAGS=-fno-rtti -std=c++14 -AM_CPPFLAGS = -D_GLFW_BUILD_DLL=1 \ +AM_CPPFLAGS = -D_GLFW_BUILD_DLL=1 -D_GLFW_X11=1 \ -I../raylib/raylib/src/external/glfw/include \ -I../raylib/raylib/src/external/glfw/deps lib_LTLIBRARIES = libnuklear.la diff --git a/nuklear/Nuklear b/nuklear/Nuklear index 8e5c9f7..055a0aa 160000 --- a/nuklear/Nuklear +++ b/nuklear/Nuklear @@ -1 +1 @@ -Subproject commit 8e5c9f7f345c7a7560f4538ffc42e7f1baff0f10 +Subproject commit 055a0aad0ff10921d638a9b93b0fe87f3a86d777 diff --git a/raylib/raygui b/raylib/raygui index 623bc61..9060e3b 160000 --- a/raylib/raygui +++ b/raylib/raygui @@ -1 +1 @@ -Subproject commit 623bc61f29839d451061252e1cc93edb013c2f8c +Subproject commit 9060e3bf33795dad4fdb4f60585b07901ffa5dee diff --git a/raylib/raylib b/raylib/raylib index 1fad827..9cf408f 160000 --- a/raylib/raylib +++ b/raylib/raylib @@ -1 +1 @@ -Subproject commit 1fad8277a37b366841eff1497ca07a7042d4a014 +Subproject commit 9cf408f77c58828fd1019d5cd35c6b8c5139c8dc diff --git a/websocket/mongoose b/websocket/mongoose index 9f498d2..d7b2b3f 160000 --- a/websocket/mongoose +++ b/websocket/mongoose @@ -1 +1 @@ -Subproject commit 9f498d2eea27737bea2f8bce47107d328e881ad0 +Subproject commit d7b2b3f42ec1109e6955baff6f836a15cfdd107a From 3fe97c57b6618ccb245c6a9519049eea7fba8ab6 Mon Sep 17 00:00:00 2001 From: chrisws Date: Sat, 23 Mar 2024 08:56:17 +1030 Subject: [PATCH 022/131] IOIO: android integration --- ioio/Makefile.am | 4 + .../AccessoryConnectionBootstrap.java | 78 +++++++++---------- .../Adapter.java => UsbManagerAdapter.java} | 51 ++++++------ .../smallbasic/ioio/ConnectionController.java | 7 +- .../smallbasic/ioio/IOIOLoader.java | 13 +++- .../sourceforge/smallbasic/ioio/IOLock.java | 6 +- ioio/main.cpp | 25 ++++-- 7 files changed, 100 insertions(+), 84 deletions(-) rename ioio/ioio/src/main/java/ioio/lib/android/{accessory => }/AccessoryConnectionBootstrap.java (90%) rename ioio/ioio/src/main/java/ioio/lib/android/{accessory/Adapter.java => UsbManagerAdapter.java} (76%) diff --git a/ioio/Makefile.am b/ioio/Makefile.am index bff2ea9..ccb1676 100644 --- a/ioio/Makefile.am +++ b/ioio/Makefile.am @@ -35,3 +35,7 @@ android: (cd ioio/build/libs && zip ../outputs/aar/ioio-debug.aar classes.jar) && \ (cd ioio/build/libs && zip ../outputs/aar/ioio-release.aar classes.jar) && \ cp ioio/build/outputs/aar/ioio-* ~/src/SmallBASIC/src/platform/android/app/libs/ + +desktop: + @(cd ioio && mvn clean package && cp target/ioio-1.0-jar-with-dependencies.jar ..) + diff --git a/ioio/ioio/src/main/java/ioio/lib/android/accessory/AccessoryConnectionBootstrap.java b/ioio/ioio/src/main/java/ioio/lib/android/AccessoryConnectionBootstrap.java similarity index 90% rename from ioio/ioio/src/main/java/ioio/lib/android/accessory/AccessoryConnectionBootstrap.java rename to ioio/ioio/src/main/java/ioio/lib/android/AccessoryConnectionBootstrap.java index 7b20634..fe70b44 100644 --- a/ioio/ioio/src/main/java/ioio/lib/android/accessory/AccessoryConnectionBootstrap.java +++ b/ioio/ioio/src/main/java/ioio/lib/android/AccessoryConnectionBootstrap.java @@ -27,15 +27,19 @@ * or implied. */ -package ioio.lib.android.accessory; +package ioio.lib.android; -import ioio.lib.android.accessory.Adapter.UsbAccessoryInterface; -import ioio.lib.api.IOIOConnection; -import ioio.lib.api.exception.ConnectionLostException; -import ioio.lib.impl.FixedReadBufferedInputStream; -import ioio.lib.spi.IOIOConnectionBootstrap; -import ioio.lib.spi.IOIOConnectionFactory; -import ioio.lib.spi.NoRuntimeSupportException; +import android.annotation.TargetApi; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Build; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import net.sourceforge.smallbasic.ioio.IOIOLoader; import java.io.BufferedOutputStream; import java.io.FileDescriptor; @@ -46,22 +50,20 @@ import java.io.OutputStream; import java.util.Collection; -import android.app.PendingIntent; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.ContextWrapper; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.ParcelFileDescriptor; -import android.util.Log; +import ioio.lib.android.UsbManagerAdapter.UsbAccessoryInterface; +import ioio.lib.api.IOIOConnection; +import ioio.lib.api.exception.ConnectionLostException; +import ioio.lib.impl.FixedReadBufferedInputStream; +import ioio.lib.spi.IOIOConnectionBootstrap; +import ioio.lib.spi.IOIOConnectionFactory; +import ioio.lib.spi.NoRuntimeSupportException; public class AccessoryConnectionBootstrap extends BroadcastReceiver implements IOIOConnectionBootstrap, IOIOConnectionFactory { private static final String TAG = AccessoryConnectionBootstrap.class.getSimpleName(); private static final String ACTION_USB_PERMISSION = "ioio.lib.accessory.action.USB_PERMISSION"; - private ContextWrapper activity; - private final Adapter adapter; - private Adapter.AbstractUsbManager usbManager; + private final Context activity; + private final UsbManagerAdapter.AbstractUsbManager usbManager; private boolean shouldTryOpen = false; private PendingIntent pendingIntent; private ParcelFileDescriptor fileDescriptor; @@ -69,19 +71,16 @@ public class AccessoryConnectionBootstrap extends BroadcastReceiver implements I private OutputStream outputStream; public AccessoryConnectionBootstrap() throws NoRuntimeSupportException { - adapter = new Adapter(); - } - - //@Override - public void onCreate(ContextWrapper wrapper) { - activity = wrapper; - usbManager = adapter.getManager(wrapper); + Log.d(TAG, "creating AccessoryConnectionBootstrap"); + UsbManagerAdapter usbManagerAdapter = new UsbManagerAdapter(); + activity = IOIOLoader.getContext(); + usbManager = usbManagerAdapter.getManager(activity); registerReceiver(); } //@Override public void onDestroy() { - unregisterReceiver(); + activity.unregisterReceiver(this); } @Override @@ -107,12 +106,9 @@ public synchronized void reopen() { notifyAll(); } - //@Override - public synchronized void close() { - } - private synchronized void disconnect() { // This should abort any current open attempt. + Log.d(TAG, "private disconnect"); shouldTryOpen = false; notifyAll(); @@ -130,6 +126,7 @@ private synchronized void disconnect() { pendingIntent.cancel(); pendingIntent = null; } + Log.d(TAG, "leaving private disconnect"); } @Override @@ -257,13 +254,12 @@ private boolean tryOpen() { } } + @TargetApi(Build.VERSION_CODES.TIRAMISU) private void registerReceiver() { IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION); - activity.registerReceiver(this, filter); - } - - private void unregisterReceiver() { - activity.unregisterReceiver(this); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + activity.registerReceiver(this, filter, Context.RECEIVER_NOT_EXPORTED); + } } private void trySleep(long time) { @@ -271,24 +267,25 @@ private void trySleep(long time) { try { AccessoryConnectionBootstrap.this.wait(time); } catch (InterruptedException e) { + Log.e(TAG, e.toString()); } } } - private static enum InstanceState { + private enum InstanceState { INIT, CONNECTED, DEAD - }; + } private class Connection implements IOIOConnection { private InstanceState instanceState_ = InstanceState.INIT; @Override - public InputStream getInputStream() throws ConnectionLostException { + public InputStream getInputStream() { return inputStream; } @Override - public OutputStream getOutputStream() throws ConnectionLostException { + public OutputStream getOutputStream() { return outputStream; } @@ -316,6 +313,7 @@ public void waitForConnect() throws ConnectionLostException { @Override public void disconnect() { + Log.d(TAG, "disconnect"); synchronized(AccessoryConnectionBootstrap.this) { if (instanceState_ != InstanceState.DEAD) { AccessoryConnectionBootstrap.this.disconnect(); diff --git a/ioio/ioio/src/main/java/ioio/lib/android/accessory/Adapter.java b/ioio/ioio/src/main/java/ioio/lib/android/UsbManagerAdapter.java similarity index 76% rename from ioio/ioio/src/main/java/ioio/lib/android/accessory/Adapter.java rename to ioio/ioio/src/main/java/ioio/lib/android/UsbManagerAdapter.java index 69c89c5..cf337b5 100644 --- a/ioio/ioio/src/main/java/ioio/lib/android/accessory/Adapter.java +++ b/ioio/ioio/src/main/java/ioio/lib/android/UsbManagerAdapter.java @@ -27,50 +27,44 @@ * or implied. */ -package ioio.lib.android.accessory; +package ioio.lib.android; -import ioio.lib.spi.NoRuntimeSupportException; import android.app.PendingIntent; import android.content.Context; -import android.content.ContextWrapper; - import android.hardware.usb.UsbAccessory; import android.hardware.usb.UsbManager; import android.os.ParcelFileDescriptor; +import ioio.lib.spi.Log; +import ioio.lib.spi.NoRuntimeSupportException; + /** - * This class serves as a common interface for USB accessory for either com.android.future.usb or - * - * When this class is instantiated, it will throw a {@link NoRuntimeSupportException} if none of the - * underlying implementations exist. Otherwise, the client may call - * {@link #getManager(ContextWrapper)}, which returns an interface that is equivalent to UsbManager - * on either API. Likewise, the {@link UsbAccessoryInterface} interface is a wrapper around - * UsbAccessory. + * This class serves as a common interface for USB accessory for android.hardware.usb.UsbManager * + * When this class is instantiated, it will throw a {@link NoRuntimeSupportException} if the + * underlying implementation does not exist. */ -class Adapter { - Adapter() throws NoRuntimeSupportException { +class UsbManagerAdapter { + private static final String TAG = UsbManagerAdapter.class.getSimpleName(); + + UsbManagerAdapter() throws NoRuntimeSupportException { try { - Class.forName("UsbManager"); + Class.forName("android.hardware.usb.UsbManager"); } catch (ClassNotFoundException e) { - throw new NoRuntimeSupportException("No support for USB accesory mode."); + Log.d(TAG, e.toString()); + throw new NoRuntimeSupportException("No support for USB accessory mode."); } } - AbstractUsbManager getManager(ContextWrapper wrapper) { - return getManagerNew(wrapper); - } - - private AbstractUsbManager getManagerNew(ContextWrapper wrapper) { + AbstractUsbManager getManager(Context wrapper) { final UsbManager manager = (UsbManager) wrapper.getSystemService(Context.USB_SERVICE); - return new NewUsbManager(manager); + return new UsbManagerImpl(manager); } static abstract class AbstractUsbManager { final String EXTRA_PERMISSION_GRANTED; - protected AbstractUsbManager(String action_usb_accessory_detached, - String extra_permission_granted) { - EXTRA_PERMISSION_GRANTED = extra_permission_granted; + protected AbstractUsbManager() { + EXTRA_PERMISSION_GRANTED = UsbManager.EXTRA_PERMISSION_GRANTED; } abstract UsbAccessoryInterface[] getAccessoryList(); @@ -79,12 +73,11 @@ protected AbstractUsbManager(String action_usb_accessory_detached, abstract void requestPermission(UsbAccessoryInterface accessory, PendingIntent pendingIntent); } - private static final class NewUsbManager extends AbstractUsbManager { + private static final class UsbManagerImpl extends AbstractUsbManager { private final UsbManager manager; - private NewUsbManager(UsbManager manager) { - super(UsbManager.ACTION_USB_ACCESSORY_DETACHED, - UsbManager.EXTRA_PERMISSION_GRANTED); + private UsbManagerImpl(UsbManager manager) { + super(); this.manager = manager; } @@ -127,6 +120,6 @@ private static class UsbAccessoryAdapter implements UsbAccessoryInterface { } } - static interface UsbAccessoryInterface { + interface UsbAccessoryInterface { } } diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/ConnectionController.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/ConnectionController.java index 439f471..77d9564 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/ConnectionController.java +++ b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/ConnectionController.java @@ -11,12 +11,9 @@ public class ConnectionController extends IOIOBaseApplicationHelper { static { if (AndroidUtil.isAndroid()) { - IOIOConnectionRegistry.addBootstraps(new String[]{ - "ioio.lib.android.accessory.AccessoryConnectionBootstrap"}); + IOIOConnectionRegistry.addBootstraps(new String[]{"ioio.lib.android.AccessoryConnectionBootstrap"}); } else { - IOIOConnectionRegistry.addBootstraps(new String[]{ - "ioio.lib.pc.SerialPortIOIOConnectionBootstrap" - }); + IOIOConnectionRegistry.addBootstraps(new String[]{"ioio.lib.pc.SerialPortIOIOConnectionBootstrap"}); } } diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOLoader.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOLoader.java index 9b40750..a43d3e5 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOLoader.java +++ b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOLoader.java @@ -1,14 +1,25 @@ package net.sourceforge.smallbasic.ioio; +import android.content.Context; + +import java.lang.ref.WeakReference; + import ioio.lib.spi.Log; public class IOIOLoader { private static final String TAG = "IOIOLoader"; public static native void init(Long app); - public IOIOLoader(Long activity) { + private static WeakReference context; + + public IOIOLoader(Long activity, Context context) { super(); Log.d(TAG, "IOIOLoader: " + activity); + IOIOLoader.context = new WeakReference<>(context); init(activity); } + + public static Context getContext() { + return context.get(); + } } diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOLock.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOLock.java index 9cfd29e..0200716 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOLock.java +++ b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOLock.java @@ -1,6 +1,7 @@ package net.sourceforge.smallbasic.ioio; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; @@ -10,6 +11,7 @@ public class IOLock { private final Object mutex = new Object(); + private static final int TIMEOUT_SECS = 5; private Consumer consumer; public void invoke(Consumer consumer) { @@ -87,7 +89,9 @@ private CountDownLatch beginLatch() { */ private void endLatch(CountDownLatch latch) { try { - latch.await(); + if (!latch.await(TIMEOUT_SECS, TimeUnit.SECONDS)) { + throw new RuntimeException("Timeout waiting for latch"); + } } catch (InterruptedException e) { throw new RuntimeException(e); } diff --git a/ioio/main.cpp b/ioio/main.cpp index 889bc43..2eb5285 100644 --- a/ioio/main.cpp +++ b/ioio/main.cpp @@ -134,12 +134,18 @@ struct IOTask { auto exc = g_env->ExceptionOccurred(); if (exc) { if (retval) { +#if defined(ANDROID_MODULE) + // avoid: JNI DETECTED ERROR IN APPLICATION: JNI FindClass called with pending exception + error(retval, "Java exception - see adb logcat"); + g_env->ExceptionClear(); +#else jclass clazz = g_env->FindClass("java/lang/Object"); jmethodID methodId = g_env->GetMethodID(clazz, "toString", "()Ljava/lang/String;"); jstring jstr = (jstring) g_env->CallObjectMethod(exc, methodId); const char *message = g_env->GetStringUTFChars(jstr, JNI_FALSE); error(retval, message); g_env->ReleaseStringUTFChars(jstr, message); +#endif } else { g_env->ExceptionDescribe(); g_env->ExceptionClear(); @@ -513,12 +519,15 @@ int sblib_init(const char *sourceFile) { #else int result = 1; #endif - - g_ioioTask = new IOTask(); - var_t retval; - if (!g_ioioTask || !g_ioioTask->create(CLASS_IOIO, &retval)) { - fprintf(stderr, "Failed to IOIOTask: %s\n", v_getstr(&retval)); + if (g_jvm == nullptr) { result = 0; + } else { + g_ioioTask = new IOTask(); + var_t retval; + if (!g_ioioTask || !g_ioioTask->create(CLASS_IOIO, &retval)) { + fprintf(stderr, "Failed to IOIOTask: %s\n", v_getstr(&retval)); + result = 0; + } } return result; } @@ -526,7 +535,7 @@ int sblib_init(const char *sourceFile) { #if defined(ANDROID_MODULE) // // Stores the Android JavaVM reference -// +// extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void* reserved) { logEntered(); g_jvm = vm; @@ -583,7 +592,7 @@ void sblib_close(void) { fprintf(stderr, "IOTask leak detected\n"); g_ioTaskMap.clear(); } -#if defined(DESKTOP_MODULE) +#if defined(DESKTOP_MODULE) if (g_jvm) { g_jvm->DetachCurrentThread(); } @@ -591,5 +600,5 @@ void sblib_close(void) { //jvm->DestroyJavaVM(); g_env = nullptr; g_jvm = nullptr; -#endif +#endif } From 6a4dcbfcb0672fd9649d0e939acbc930e7505f88 Mon Sep 17 00:00:00 2001 From: chrisws Date: Sat, 23 Mar 2024 10:00:46 +1030 Subject: [PATCH 023/131] IOIO: android integration --- ioio/ioio/src/main/AndroidManifest.xml | 5 +++++ .../lib/android/AccessoryConnectionBootstrap.java | 11 +++++------ ioio/main.cpp | 5 ----- 3 files changed, 10 insertions(+), 11 deletions(-) create mode 100644 ioio/ioio/src/main/AndroidManifest.xml diff --git a/ioio/ioio/src/main/AndroidManifest.xml b/ioio/ioio/src/main/AndroidManifest.xml new file mode 100644 index 0000000..afb2990 --- /dev/null +++ b/ioio/ioio/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + diff --git a/ioio/ioio/src/main/java/ioio/lib/android/AccessoryConnectionBootstrap.java b/ioio/ioio/src/main/java/ioio/lib/android/AccessoryConnectionBootstrap.java index fe70b44..0da2d9c 100644 --- a/ioio/ioio/src/main/java/ioio/lib/android/AccessoryConnectionBootstrap.java +++ b/ioio/ioio/src/main/java/ioio/lib/android/AccessoryConnectionBootstrap.java @@ -37,7 +37,6 @@ import android.content.IntentFilter; import android.os.Build; import android.os.ParcelFileDescriptor; -import android.util.Log; import net.sourceforge.smallbasic.ioio.IOIOLoader; @@ -56,6 +55,7 @@ import ioio.lib.impl.FixedReadBufferedInputStream; import ioio.lib.spi.IOIOConnectionBootstrap; import ioio.lib.spi.IOIOConnectionFactory; +import ioio.lib.spi.Log; import ioio.lib.spi.NoRuntimeSupportException; public class AccessoryConnectionBootstrap extends BroadcastReceiver implements IOIOConnectionBootstrap, IOIOConnectionFactory { @@ -191,8 +191,7 @@ private boolean tryOpen() { if (!usbManager.hasPermission(accessory)) { if (pendingIntent == null) { Log.v(TAG, "Requesting permission."); - pendingIntent = PendingIntent.getBroadcast(activity, 0, new Intent( - ACTION_USB_PERMISSION), 0); + pendingIntent = PendingIntent.getBroadcast(activity, 0, new Intent(ACTION_USB_PERMISSION), 0); usbManager.requestPermission(accessory, pendingIntent); } return false; @@ -228,7 +227,7 @@ private boolean tryOpen() { // bug: // http://code.google.com/p/android/issues/detail?id=20545 while (inputStream.read() != 1) { - trySleep(1000); + trySleep(); } success = true; @@ -262,10 +261,10 @@ private void registerReceiver() { } } - private void trySleep(long time) { + private void trySleep() { synchronized (AccessoryConnectionBootstrap.this) { try { - AccessoryConnectionBootstrap.this.wait(time); + AccessoryConnectionBootstrap.this.wait(1000); } catch (InterruptedException e) { Log.e(TAG, e.toString()); } diff --git a/ioio/main.cpp b/ioio/main.cpp index 2eb5285..1db6a63 100644 --- a/ioio/main.cpp +++ b/ioio/main.cpp @@ -134,18 +134,13 @@ struct IOTask { auto exc = g_env->ExceptionOccurred(); if (exc) { if (retval) { -#if defined(ANDROID_MODULE) - // avoid: JNI DETECTED ERROR IN APPLICATION: JNI FindClass called with pending exception - error(retval, "Java exception - see adb logcat"); g_env->ExceptionClear(); -#else jclass clazz = g_env->FindClass("java/lang/Object"); jmethodID methodId = g_env->GetMethodID(clazz, "toString", "()Ljava/lang/String;"); jstring jstr = (jstring) g_env->CallObjectMethod(exc, methodId); const char *message = g_env->GetStringUTFChars(jstr, JNI_FALSE); error(retval, message); g_env->ReleaseStringUTFChars(jstr, message); -#endif } else { g_env->ExceptionDescribe(); g_env->ExceptionClear(); From 15301b0cf7b75aca8c860495935451e38fd70e00 Mon Sep 17 00:00:00 2001 From: chrisws Date: Sat, 23 Mar 2024 14:45:10 +1030 Subject: [PATCH 024/131] IOIO: android integration --- .../java/ioio/lib/android/AccessoryConnectionBootstrap.java | 2 -- .../smallbasic/ioio => ioio/lib/android}/IOIOLoader.java | 2 +- .../src/main/java/net/sourceforge/smallbasic/ioio/IOLock.java | 4 ++-- ioio/main.cpp | 4 ++-- 4 files changed, 5 insertions(+), 7 deletions(-) rename ioio/ioio/src/main/java/{net/sourceforge/smallbasic/ioio => ioio/lib/android}/IOIOLoader.java (92%) diff --git a/ioio/ioio/src/main/java/ioio/lib/android/AccessoryConnectionBootstrap.java b/ioio/ioio/src/main/java/ioio/lib/android/AccessoryConnectionBootstrap.java index 0da2d9c..15e5402 100644 --- a/ioio/ioio/src/main/java/ioio/lib/android/AccessoryConnectionBootstrap.java +++ b/ioio/ioio/src/main/java/ioio/lib/android/AccessoryConnectionBootstrap.java @@ -38,8 +38,6 @@ import android.os.Build; import android.os.ParcelFileDescriptor; -import net.sourceforge.smallbasic.ioio.IOIOLoader; - import java.io.BufferedOutputStream; import java.io.FileDescriptor; import java.io.FileInputStream; diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOLoader.java b/ioio/ioio/src/main/java/ioio/lib/android/IOIOLoader.java similarity index 92% rename from ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOLoader.java rename to ioio/ioio/src/main/java/ioio/lib/android/IOIOLoader.java index a43d3e5..a49d14b 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOLoader.java +++ b/ioio/ioio/src/main/java/ioio/lib/android/IOIOLoader.java @@ -1,4 +1,4 @@ -package net.sourceforge.smallbasic.ioio; +package ioio.lib.android; import android.content.Context; diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOLock.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOLock.java index 0200716..ce7613b 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOLock.java +++ b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOLock.java @@ -11,7 +11,7 @@ public class IOLock { private final Object mutex = new Object(); - private static final int TIMEOUT_SECS = 5; + private static final int TIMEOUT_SECS = 20; private Consumer consumer; public void invoke(Consumer consumer) { @@ -90,7 +90,7 @@ private CountDownLatch beginLatch() { private void endLatch(CountDownLatch latch) { try { if (!latch.await(TIMEOUT_SECS, TimeUnit.SECONDS)) { - throw new RuntimeException("Timeout waiting for latch"); + throw new RuntimeException("Timeout waiting for device"); } } catch (InterruptedException e) { throw new RuntimeException(e); diff --git a/ioio/main.cpp b/ioio/main.cpp index 1db6a63..aca3fa6 100644 --- a/ioio/main.cpp +++ b/ioio/main.cpp @@ -76,7 +76,7 @@ jclass findClass(const char *path) { return g_env->FindClass(path); } -jobject createInstance() { +jobject createInstance(jclass clazz) { jmethodID constructor = g_env->GetMethodID(clazz, "", "()V"); jobject result; if (constructor != nullptr) { @@ -496,7 +496,7 @@ int sblib_init(const char *sourceFile) { #if defined(DESKTOP_MODULE) JavaVMInitArgs vm_args; JavaVMOption options[3]; - options[0].optionString = (char *)"-Djava.class.path=./android/target/ioio-1.0-jar-with-dependencies.jar"; + options[0].optionString = (char *)"-Djava.class.path=./ioio-1.0-jar-with-dependencies.jar"; options[1].optionString = (char *)"-Dioio.SerialPorts=IOIO0"; options[2].optionString = (char *)"-Xrs"; //options[2].optionString = "-Xdebug"; From 72614d4bf93c9e87b162204836f9d655845160ca Mon Sep 17 00:00:00 2001 From: chrisws Date: Sat, 23 Mar 2024 20:33:33 +1030 Subject: [PATCH 025/131] IOIO: minor refactoring --- ioio/Makefile.am | 2 +- .../android/AccessoryConnectionBootstrap.java | 151 +++++++++--------- .../java/ioio/lib/android/AndroidUtil.java | 23 --- .../java/ioio/lib/android/IOIOLoader.java | 3 + .../ioio/lib/android/UsbManagerAdapter.java | 125 --------------- .../src/main/java/ioio/lib/spi/LogImpl.java | 3 +- .../smallbasic/ioio/ConnectionController.java | 14 +- .../sourceforge/smallbasic/ioio/IOLock.java | 2 +- 8 files changed, 94 insertions(+), 229 deletions(-) delete mode 100644 ioio/ioio/src/main/java/ioio/lib/android/AndroidUtil.java delete mode 100644 ioio/ioio/src/main/java/ioio/lib/android/UsbManagerAdapter.java diff --git a/ioio/Makefile.am b/ioio/Makefile.am index ccb1676..3adb533 100644 --- a/ioio/Makefile.am +++ b/ioio/Makefile.am @@ -36,6 +36,6 @@ android: (cd ioio/build/libs && zip ../outputs/aar/ioio-release.aar classes.jar) && \ cp ioio/build/outputs/aar/ioio-* ~/src/SmallBASIC/src/platform/android/app/libs/ -desktop: +desktop: all @(cd ioio && mvn clean package && cp target/ioio-1.0-jar-with-dependencies.jar ..) diff --git a/ioio/ioio/src/main/java/ioio/lib/android/AccessoryConnectionBootstrap.java b/ioio/ioio/src/main/java/ioio/lib/android/AccessoryConnectionBootstrap.java index 15e5402..fb174f1 100644 --- a/ioio/ioio/src/main/java/ioio/lib/android/AccessoryConnectionBootstrap.java +++ b/ioio/ioio/src/main/java/ioio/lib/android/AccessoryConnectionBootstrap.java @@ -35,6 +35,8 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.hardware.usb.UsbAccessory; +import android.hardware.usb.UsbManager; import android.os.Build; import android.os.ParcelFileDescriptor; @@ -47,7 +49,6 @@ import java.io.OutputStream; import java.util.Collection; -import ioio.lib.android.UsbManagerAdapter.UsbAccessoryInterface; import ioio.lib.api.IOIOConnection; import ioio.lib.api.exception.ConnectionLostException; import ioio.lib.impl.FixedReadBufferedInputStream; @@ -59,23 +60,47 @@ public class AccessoryConnectionBootstrap extends BroadcastReceiver implements IOIOConnectionBootstrap, IOIOConnectionFactory { private static final String TAG = AccessoryConnectionBootstrap.class.getSimpleName(); private static final String ACTION_USB_PERMISSION = "ioio.lib.accessory.action.USB_PERMISSION"; + private static final String EXTRA_PERMISSION_GRANTED = UsbManager.EXTRA_PERMISSION_GRANTED; private final Context activity; - private final UsbManagerAdapter.AbstractUsbManager usbManager; + private final UsbManager usbManager; private boolean shouldTryOpen = false; private PendingIntent pendingIntent; private ParcelFileDescriptor fileDescriptor; private InputStream inputStream; private OutputStream outputStream; + private enum InstanceState { + INIT, CONNECTED, DEAD + } + public AccessoryConnectionBootstrap() throws NoRuntimeSupportException { Log.d(TAG, "creating AccessoryConnectionBootstrap"); - UsbManagerAdapter usbManagerAdapter = new UsbManagerAdapter(); - activity = IOIOLoader.getContext(); - usbManager = usbManagerAdapter.getManager(activity); + this.activity = IOIOLoader.getContext(); + this.usbManager = (UsbManager) activity.getSystemService(Context.USB_SERVICE); registerReceiver(); } + @Override + public IOIOConnection createConnection() { + return new Connection(); + } + + @Override + public Object getExtra() { + return null; + } + + @Override + public void getFactories(Collection result) { + result.add(this); + } + + @Override + public String getType() { + return Connection.class.getCanonicalName(); + } + //@Override public void onDestroy() { activity.unregisterReceiver(this); @@ -86,7 +111,7 @@ public synchronized void onReceive(Context context, Intent intent) { final String action = intent.getAction(); if (ACTION_USB_PERMISSION.equals(action)) { pendingIntent = null; - if (intent.getBooleanExtra(usbManager.EXTRA_PERMISSION_GRANTED, false)) { + if (intent.getBooleanExtra(EXTRA_PERMISSION_GRANTED, false)) { notifyAll(); } else { Log.e(TAG, "Permission denied"); @@ -127,46 +152,6 @@ private synchronized void disconnect() { Log.d(TAG, "leaving private disconnect"); } - @Override - public IOIOConnection createConnection() { - return new Connection(); - } - - @Override - public void getFactories(Collection result) { - result.add(this); - } - - @Override - public String getType() { - return Connection.class.getCanonicalName(); - } - - @Override - public Object getExtra() { - return null; - } - - private synchronized void waitForConnect(Connection connection) throws ConnectionLostException { - // In order to simplify the connection process in face of many different sequences of events - // that might occur, we collapsed the entire sequence into one non-blocking method, - // tryOpen(), which tries the entire process from the beginning, undoes everything if - // something along the way fails and always returns immediately. - // This method, simply calls tryOpen() in a loop until it succeeds or until we're no longer - // interested. Between attempts, it waits until "something interesting" has happened, which - // may be permission granted, the client telling us to try again (via reopen()) or stop - // trying, etc. - shouldTryOpen = true; - while (shouldTryOpen) { - if (tryOpen()) { - // Success! - return; - } - forceWait(); - } - throw new ConnectionLostException(); - } - private void forceWait() { try { wait(); @@ -175,10 +160,18 @@ private void forceWait() { } } + @TargetApi(Build.VERSION_CODES.TIRAMISU) + private void registerReceiver() { + IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + activity.registerReceiver(this, filter, Context.RECEIVER_NOT_EXPORTED); + } + } + private boolean tryOpen() { // Find the accessory. - UsbAccessoryInterface[] accessories = usbManager.getAccessoryList(); - UsbAccessoryInterface accessory = (accessories == null ? null : accessories[0]); + UsbAccessory[] accessories = usbManager.getAccessoryList(); + UsbAccessory accessory = (accessories == null ? null : accessories[0]); if (accessory == null) { Log.v(TAG, "No accessory found."); @@ -251,14 +244,6 @@ private boolean tryOpen() { } } - @TargetApi(Build.VERSION_CODES.TIRAMISU) - private void registerReceiver() { - IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - activity.registerReceiver(this, filter, Context.RECEIVER_NOT_EXPORTED); - } - } - private void trySleep() { synchronized (AccessoryConnectionBootstrap.this) { try { @@ -269,13 +254,45 @@ private void trySleep() { } } - private enum InstanceState { - INIT, CONNECTED, DEAD + private synchronized void waitForConnect(Connection connection) throws ConnectionLostException { + // In order to simplify the connection process in face of many different sequences of events + // that might occur, we collapsed the entire sequence into one non-blocking method, + // tryOpen(), which tries the entire process from the beginning, undoes everything if + // something along the way fails and always returns immediately. + // This method, simply calls tryOpen() in a loop until it succeeds or until we're no longer + // interested. Between attempts, it waits until "something interesting" has happened, which + // may be permission granted, the client telling us to try again (via reopen()) or stop + // trying, etc. + shouldTryOpen = true; + while (shouldTryOpen) { + if (tryOpen()) { + // Success! + return; + } + forceWait(); + } + throw new ConnectionLostException(); } private class Connection implements IOIOConnection { private InstanceState instanceState_ = InstanceState.INIT; + @Override + public boolean canClose() { + return false; + } + + @Override + public void disconnect() { + Log.d(TAG, "disconnect"); + synchronized(AccessoryConnectionBootstrap.this) { + if (instanceState_ != InstanceState.DEAD) { + AccessoryConnectionBootstrap.this.disconnect(); + instanceState_ = InstanceState.DEAD; + } + } + } + @Override public InputStream getInputStream() { return inputStream; @@ -286,11 +303,6 @@ public OutputStream getOutputStream() { return outputStream; } - @Override - public boolean canClose() { - return false; - } - @Override public void waitForConnect() throws ConnectionLostException { synchronized(AccessoryConnectionBootstrap.this) { @@ -309,18 +321,7 @@ public void waitForConnect() throws ConnectionLostException { } @Override - public void disconnect() { - Log.d(TAG, "disconnect"); - synchronized(AccessoryConnectionBootstrap.this) { - if (instanceState_ != InstanceState.DEAD) { - AccessoryConnectionBootstrap.this.disconnect(); - instanceState_ = InstanceState.DEAD; - } - } - } - - @Override - protected void finalize() throws Throwable { + protected void finalize() { disconnect(); } } diff --git a/ioio/ioio/src/main/java/ioio/lib/android/AndroidUtil.java b/ioio/ioio/src/main/java/ioio/lib/android/AndroidUtil.java deleted file mode 100644 index 4ceeb7d..0000000 --- a/ioio/ioio/src/main/java/ioio/lib/android/AndroidUtil.java +++ /dev/null @@ -1,23 +0,0 @@ -package ioio.lib.android; - -public class AndroidUtil { - private static final boolean isRunningOnAndroid; - static { - isRunningOnAndroid = getIsRunningOnAndroid(); - } - - public static boolean isAndroid() { - return isRunningOnAndroid; - } - - private static boolean getIsRunningOnAndroid() { - boolean result; - try { - Class.forName("android.os.Build"); - result = true; - } catch (ClassNotFoundException e) { - result = false; - } - return result; - } -} diff --git a/ioio/ioio/src/main/java/ioio/lib/android/IOIOLoader.java b/ioio/ioio/src/main/java/ioio/lib/android/IOIOLoader.java index a49d14b..df0bdf1 100644 --- a/ioio/ioio/src/main/java/ioio/lib/android/IOIOLoader.java +++ b/ioio/ioio/src/main/java/ioio/lib/android/IOIOLoader.java @@ -6,6 +6,9 @@ import ioio.lib.spi.Log; +/** + * IOIOLoader - Invoked from the "app" to commence loading + */ public class IOIOLoader { private static final String TAG = "IOIOLoader"; public static native void init(Long app); diff --git a/ioio/ioio/src/main/java/ioio/lib/android/UsbManagerAdapter.java b/ioio/ioio/src/main/java/ioio/lib/android/UsbManagerAdapter.java deleted file mode 100644 index cf337b5..0000000 --- a/ioio/ioio/src/main/java/ioio/lib/android/UsbManagerAdapter.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright 2014 Ytai Ben-Tsvi. All rights reserved. - * - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED - * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND - * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARSHAN POURSOHI OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * The views and conclusions contained in the software and documentation are those of the - * authors and should not be interpreted as representing official policies, either expressed - * or implied. - */ - -package ioio.lib.android; - -import android.app.PendingIntent; -import android.content.Context; -import android.hardware.usb.UsbAccessory; -import android.hardware.usb.UsbManager; -import android.os.ParcelFileDescriptor; - -import ioio.lib.spi.Log; -import ioio.lib.spi.NoRuntimeSupportException; - -/** - * This class serves as a common interface for USB accessory for android.hardware.usb.UsbManager - * - * When this class is instantiated, it will throw a {@link NoRuntimeSupportException} if the - * underlying implementation does not exist. - */ -class UsbManagerAdapter { - private static final String TAG = UsbManagerAdapter.class.getSimpleName(); - - UsbManagerAdapter() throws NoRuntimeSupportException { - try { - Class.forName("android.hardware.usb.UsbManager"); - } catch (ClassNotFoundException e) { - Log.d(TAG, e.toString()); - throw new NoRuntimeSupportException("No support for USB accessory mode."); - } - } - - AbstractUsbManager getManager(Context wrapper) { - final UsbManager manager = (UsbManager) wrapper.getSystemService(Context.USB_SERVICE); - return new UsbManagerImpl(manager); - } - - static abstract class AbstractUsbManager { - final String EXTRA_PERMISSION_GRANTED; - protected AbstractUsbManager() { - EXTRA_PERMISSION_GRANTED = UsbManager.EXTRA_PERMISSION_GRANTED; - } - - abstract UsbAccessoryInterface[] getAccessoryList(); - abstract boolean hasPermission(UsbAccessoryInterface accessory); - abstract ParcelFileDescriptor openAccessory(UsbAccessoryInterface accessory); - abstract void requestPermission(UsbAccessoryInterface accessory, PendingIntent pendingIntent); - } - - private static final class UsbManagerImpl extends AbstractUsbManager { - private final UsbManager manager; - - private UsbManagerImpl(UsbManager manager) { - super(); - this.manager = manager; - } - - @Override - UsbAccessoryInterface[] getAccessoryList() { - final UsbAccessory[] accessoryList = manager.getAccessoryList(); - if (accessoryList == null) { - return null; - } - UsbAccessoryInterface[] result = new UsbAccessoryInterface[accessoryList.length]; - for (int i = 0; i < accessoryList.length; ++i) { - result[i] = new UsbAccessoryAdapter<>(accessoryList[i]); - } - return result; - } - - @SuppressWarnings("unchecked") - @Override - boolean hasPermission(UsbAccessoryInterface accessory) { - return manager.hasPermission(((UsbAccessoryAdapter) accessory).impl); - } - - @SuppressWarnings("unchecked") - @Override - ParcelFileDescriptor openAccessory(UsbAccessoryInterface accessory) { - return manager.openAccessory(((UsbAccessoryAdapter) accessory).impl); - } - - @SuppressWarnings("unchecked") - @Override - void requestPermission(UsbAccessoryInterface accessory, PendingIntent pendingIntent) { - manager.requestPermission(((UsbAccessoryAdapter) accessory).impl, pendingIntent); - } - } - - private static class UsbAccessoryAdapter implements UsbAccessoryInterface { - final T impl; - UsbAccessoryAdapter(T t) { - impl = t; - } - } - - interface UsbAccessoryInterface { - } -} diff --git a/ioio/ioio/src/main/java/ioio/lib/spi/LogImpl.java b/ioio/ioio/src/main/java/ioio/lib/spi/LogImpl.java index 4f38a43..a9822c8 100644 --- a/ioio/ioio/src/main/java/ioio/lib/spi/LogImpl.java +++ b/ioio/ioio/src/main/java/ioio/lib/spi/LogImpl.java @@ -28,12 +28,11 @@ */ package ioio.lib.spi; -import ioio.lib.android.AndroidUtil; import ioio.lib.spi.Log.ILogger; public class LogImpl implements ILogger { private static final char[] LEVELS = {'0', '1', 'V', 'D', 'I', 'W', 'E', 'F'}; - private static final ILogger logger = AndroidUtil.isAndroid() ? getAndroidLogger() : null; + private static final ILogger logger = getAndroidLogger(); @Override public void write(int level, String tag, String message) { diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/ConnectionController.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/ConnectionController.java index 77d9564..2ae741a 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/ConnectionController.java +++ b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/ConnectionController.java @@ -1,6 +1,5 @@ package net.sourceforge.smallbasic.ioio; -import ioio.lib.android.AndroidUtil; import ioio.lib.util.IOIOBaseApplicationHelper; import ioio.lib.util.IOIOConnectionManager; import ioio.lib.util.IOIOConnectionRegistry; @@ -10,7 +9,7 @@ public class ConnectionController extends IOIOBaseApplicationHelper { private final IOIOConnectionManager manager = new IOIOConnectionManager(this); static { - if (AndroidUtil.isAndroid()) { + if (getIsRunningOnAndroid()) { IOIOConnectionRegistry.addBootstraps(new String[]{"ioio.lib.android.AccessoryConnectionBootstrap"}); } else { IOIOConnectionRegistry.addBootstraps(new String[]{"ioio.lib.pc.SerialPortIOIOConnectionBootstrap"}); @@ -28,4 +27,15 @@ public void start() { public void stop() { manager.stop(); } + + private static boolean getIsRunningOnAndroid() { + boolean result; + try { + Class.forName("android.os.Build"); + result = true; + } catch (ClassNotFoundException e) { + result = false; + } + return result; + } } diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOLock.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOLock.java index ce7613b..d1cdc2c 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOLock.java +++ b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOLock.java @@ -11,7 +11,7 @@ public class IOLock { private final Object mutex = new Object(); - private static final int TIMEOUT_SECS = 20; + private static final int TIMEOUT_SECS = 5; private Consumer consumer; public void invoke(Consumer consumer) { From 835f4c1313ca7dedeea3992b3640c456d5a4e364 Mon Sep 17 00:00:00 2001 From: chrisws Date: Mon, 25 Mar 2024 20:25:21 +1030 Subject: [PATCH 026/131] IOIO: changed the package to "ioio.smallbasic" --- ioio/Makefile.am | 2 +- ioio/ioio/build.gradle | 2 +- ioio/ioio/pom.xml | 2 +- ioio/ioio/src/main/AndroidManifest.xml | 3 +- .../src/main/java/ioio/lib/spi/LogImpl.java | 2 +- .../smallbasic}/AnalogInputImpl.java | 2 +- .../smallbasic}/CapSenseImpl.java | 2 +- .../ioio/smallbasic/ConnectionController.java | 58 +++++++++++++ .../ioio => ioio/smallbasic}/Consumer.java | 2 +- .../smallbasic}/DigitalInputImpl.java | 2 +- .../smallbasic}/DigitalOutputImpl.java | 2 +- .../ioio => ioio/smallbasic}/Function.java | 2 +- .../java/ioio/smallbasic/IOIOException.java | 19 ++++ .../ioio => ioio/smallbasic}/IOIOImpl.java | 2 +- .../ioio => ioio/smallbasic}/IOLock.java | 8 +- .../ioio => ioio/smallbasic}/IOService.java | 2 +- .../ioio => ioio/smallbasic}/IOTask.java | 4 +- .../smallbasic}/PulseInputImpl.java | 2 +- .../smallbasic}/PwmOutputImpl.java | 2 +- .../java/ioio/smallbasic/SequencerImpl.java | 86 ++++++++++++++++++ .../smallbasic}/SpiMasterImpl.java | 2 +- .../ioio => ioio/smallbasic}/TimerUtil.java | 2 +- .../smallbasic}/TwiMasterImpl.java | 2 +- .../android/AccessoryConnectionBootstrap.java | 70 +++++++-------- .../android/AccessoryPermissionManager.java | 87 +++++++++++++++++++ .../android/AndroidLogger.java | 2 +- .../android/IOIOLoader.java | 2 +- .../pc/SerialPortIOIOConnection.java | 2 +- .../pc/SerialPortIOIOConnectionBootstrap.java | 2 +- .../smallbasic/ioio/ConnectionController.java | 41 --------- .../smallbasic}/AnalogInputTest.java | 2 +- .../smallbasic}/DigitalOutputTest.java | 4 +- ioio/main.cpp | 26 +++--- 33 files changed, 328 insertions(+), 122 deletions(-) rename ioio/ioio/src/main/java/{net/sourceforge/smallbasic/ioio => ioio/smallbasic}/AnalogInputImpl.java (97%) rename ioio/ioio/src/main/java/{net/sourceforge/smallbasic/ioio => ioio/smallbasic}/CapSenseImpl.java (97%) create mode 100644 ioio/ioio/src/main/java/ioio/smallbasic/ConnectionController.java rename ioio/ioio/src/main/java/{net/sourceforge/smallbasic/ioio => ioio/smallbasic}/Consumer.java (86%) rename ioio/ioio/src/main/java/{net/sourceforge/smallbasic/ioio => ioio/smallbasic}/DigitalInputImpl.java (96%) rename ioio/ioio/src/main/java/{net/sourceforge/smallbasic/ioio => ioio/smallbasic}/DigitalOutputImpl.java (95%) rename ioio/ioio/src/main/java/{net/sourceforge/smallbasic/ioio => ioio/smallbasic}/Function.java (81%) create mode 100644 ioio/ioio/src/main/java/ioio/smallbasic/IOIOException.java rename ioio/ioio/src/main/java/{net/sourceforge/smallbasic/ioio => ioio/smallbasic}/IOIOImpl.java (96%) rename ioio/ioio/src/main/java/{net/sourceforge/smallbasic/ioio => ioio/smallbasic}/IOLock.java (93%) rename ioio/ioio/src/main/java/{net/sourceforge/smallbasic/ioio => ioio/smallbasic}/IOService.java (98%) rename ioio/ioio/src/main/java/{net/sourceforge/smallbasic/ioio => ioio/smallbasic}/IOTask.java (94%) rename ioio/ioio/src/main/java/{net/sourceforge/smallbasic/ioio => ioio/smallbasic}/PulseInputImpl.java (97%) rename ioio/ioio/src/main/java/{net/sourceforge/smallbasic/ioio => ioio/smallbasic}/PwmOutputImpl.java (97%) create mode 100644 ioio/ioio/src/main/java/ioio/smallbasic/SequencerImpl.java rename ioio/ioio/src/main/java/{net/sourceforge/smallbasic/ioio => ioio/smallbasic}/SpiMasterImpl.java (98%) rename ioio/ioio/src/main/java/{net/sourceforge/smallbasic/ioio => ioio/smallbasic}/TimerUtil.java (92%) rename ioio/ioio/src/main/java/{net/sourceforge/smallbasic/ioio => ioio/smallbasic}/TwiMasterImpl.java (97%) rename ioio/ioio/src/main/java/ioio/{lib => smallbasic}/android/AccessoryConnectionBootstrap.java (87%) create mode 100644 ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionManager.java rename ioio/ioio/src/main/java/ioio/{lib => smallbasic}/android/AndroidLogger.java (88%) rename ioio/ioio/src/main/java/ioio/{lib => smallbasic}/android/IOIOLoader.java (94%) rename ioio/ioio/src/main/java/ioio/{lib => smallbasic}/pc/SerialPortIOIOConnection.java (98%) rename ioio/ioio/src/main/java/ioio/{lib => smallbasic}/pc/SerialPortIOIOConnectionBootstrap.java (99%) delete mode 100644 ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/ConnectionController.java rename ioio/ioio/src/test/java/{net/sourceforge/smallbasic/ioio => ioio/smallbasic}/AnalogInputTest.java (87%) rename ioio/ioio/src/test/java/{net/sourceforge/smallbasic/ioio => ioio/smallbasic}/DigitalOutputTest.java (85%) diff --git a/ioio/Makefile.am b/ioio/Makefile.am index 3adb533..98f25be 100644 --- a/ioio/Makefile.am +++ b/ioio/Makefile.am @@ -5,7 +5,7 @@ # Download the GNU Public License (GPL) from www.gnu.org # # export LD_LIBRARY_PATH=/usr/lib/jvm/java-1.8.0-openjdk-amd64/jre/lib/amd64/server:$LD_LIBRARY_PATH -# javap -s -p -cp target/ioio-1.0.jar 'net.sourceforge.smallbasic.ioio.AnalogInputImpl' +# javap -s -p -cp target/ioio-1.0.jar 'ioio.smallbasic.AnalogInputImpl' # sbasic=sbasic diff --git a/ioio/ioio/build.gradle b/ioio/ioio/build.gradle index 1decc67..3a4762f 100644 --- a/ioio/ioio/build.gradle +++ b/ioio/ioio/build.gradle @@ -7,7 +7,7 @@ android { sourceSets { main { java { - exclude 'ioio/lib/pc/**' + exclude 'ioio/smallbasic/pc/**' } } } diff --git a/ioio/ioio/pom.xml b/ioio/ioio/pom.xml index 1ca48a3..8f2333d 100644 --- a/ioio/ioio/pom.xml +++ b/ioio/ioio/pom.xml @@ -29,7 +29,7 @@ maven-compiler-plugin - ioio/lib/android/** + ioio/smallbasic/android/** diff --git a/ioio/ioio/src/main/AndroidManifest.xml b/ioio/ioio/src/main/AndroidManifest.xml index afb2990..1b7c589 100644 --- a/ioio/ioio/src/main/AndroidManifest.xml +++ b/ioio/ioio/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + diff --git a/ioio/ioio/src/main/java/ioio/lib/spi/LogImpl.java b/ioio/ioio/src/main/java/ioio/lib/spi/LogImpl.java index a9822c8..29cc48b 100644 --- a/ioio/ioio/src/main/java/ioio/lib/spi/LogImpl.java +++ b/ioio/ioio/src/main/java/ioio/lib/spi/LogImpl.java @@ -46,7 +46,7 @@ public void write(int level, String tag, String message) { private static ILogger getAndroidLogger() { ILogger result; try { - result = (ILogger) Class.forName("ioio.lib.android.AndroidLogger").newInstance(); + result = (ILogger) Class.forName("ioio.smallbasic.android.AndroidLogger").newInstance(); } catch (IllegalAccessException | InstantiationException | ClassNotFoundException e) { result = null; diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/AnalogInputImpl.java b/ioio/ioio/src/main/java/ioio/smallbasic/AnalogInputImpl.java similarity index 97% rename from ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/AnalogInputImpl.java rename to ioio/ioio/src/main/java/ioio/smallbasic/AnalogInputImpl.java index a80c148..5420e10 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/AnalogInputImpl.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/AnalogInputImpl.java @@ -1,4 +1,4 @@ -package net.sourceforge.smallbasic.ioio; +package ioio.smallbasic; import ioio.lib.api.AnalogInput; import ioio.lib.api.IOIO; diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/CapSenseImpl.java b/ioio/ioio/src/main/java/ioio/smallbasic/CapSenseImpl.java similarity index 97% rename from ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/CapSenseImpl.java rename to ioio/ioio/src/main/java/ioio/smallbasic/CapSenseImpl.java index c33513c..964df0b 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/CapSenseImpl.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/CapSenseImpl.java @@ -1,4 +1,4 @@ -package net.sourceforge.smallbasic.ioio; +package ioio.smallbasic; import ioio.lib.api.CapSense; import ioio.lib.api.IOIO; diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/ConnectionController.java b/ioio/ioio/src/main/java/ioio/smallbasic/ConnectionController.java new file mode 100644 index 0000000..0fd2e22 --- /dev/null +++ b/ioio/ioio/src/main/java/ioio/smallbasic/ConnectionController.java @@ -0,0 +1,58 @@ +package ioio.smallbasic; + +import ioio.lib.util.IOIOBaseApplicationHelper; +import ioio.lib.util.IOIOConnectionManager; +import ioio.lib.util.IOIOConnectionRegistry; +import ioio.lib.util.IOIOLooperProvider; + +public class ConnectionController extends IOIOBaseApplicationHelper { + private final IOIOConnectionManager manager = new IOIOConnectionManager(this); + private static final boolean isAndroid; + private static final String ANDROID_BOOTSTRAP = "ioio.smallbasic.android.AccessoryConnectionBootstrap"; + private static final String PERMISSION_MANAGER = "ioio.smallbasic.android.AccessoryPermissionManager"; + private static final String DESKTOP_BOOTSTRAP = "ioio.smallbasic.pc.SerialPortIOIOConnectionBootstrap"; + + static { + isAndroid = getIsAndroidBuild(); + if (isAndroid) { + IOIOConnectionRegistry.addBootstraps(new String[] { ANDROID_BOOTSTRAP }); + } else { + IOIOConnectionRegistry.addBootstraps(new String[] { DESKTOP_BOOTSTRAP }); + } + } + + public ConnectionController(IOIOLooperProvider provider) { + super(provider); + } + + public void start() { + if (isAndroid) { + validateAccessoryPermission(); + } + //manager.start(); + } + + public void stop() { + manager.stop(); + } + + private static boolean getIsAndroidBuild() { + boolean result; + try { + Class.forName(ANDROID_BOOTSTRAP); + result = true; + } catch (ClassNotFoundException e) { + result = false; + } + return result; + } + + private static void validateAccessoryPermission() { + try { + Class.forName(PERMISSION_MANAGER).newInstance(); + } + catch (Exception e) { + throw new IOIOException(e.toString()); + } + } +} diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/Consumer.java b/ioio/ioio/src/main/java/ioio/smallbasic/Consumer.java similarity index 86% rename from ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/Consumer.java rename to ioio/ioio/src/main/java/ioio/smallbasic/Consumer.java index da9c1ba..31c44bc 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/Consumer.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/Consumer.java @@ -1,4 +1,4 @@ -package net.sourceforge.smallbasic.ioio; +package ioio.smallbasic; import ioio.lib.api.exception.ConnectionLostException; import ioio.lib.api.exception.IncompatibilityException; diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/DigitalInputImpl.java b/ioio/ioio/src/main/java/ioio/smallbasic/DigitalInputImpl.java similarity index 96% rename from ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/DigitalInputImpl.java rename to ioio/ioio/src/main/java/ioio/smallbasic/DigitalInputImpl.java index 643419d..cd60df3 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/DigitalInputImpl.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/DigitalInputImpl.java @@ -1,4 +1,4 @@ -package net.sourceforge.smallbasic.ioio; +package ioio.smallbasic; import java.util.concurrent.atomic.AtomicBoolean; diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/DigitalOutputImpl.java b/ioio/ioio/src/main/java/ioio/smallbasic/DigitalOutputImpl.java similarity index 95% rename from ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/DigitalOutputImpl.java rename to ioio/ioio/src/main/java/ioio/smallbasic/DigitalOutputImpl.java index 75c3ca8..98acbfb 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/DigitalOutputImpl.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/DigitalOutputImpl.java @@ -1,4 +1,4 @@ -package net.sourceforge.smallbasic.ioio; +package ioio.smallbasic; import ioio.lib.api.DigitalOutput; import ioio.lib.api.IOIO; diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/Function.java b/ioio/ioio/src/main/java/ioio/smallbasic/Function.java similarity index 81% rename from ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/Function.java rename to ioio/ioio/src/main/java/ioio/smallbasic/Function.java index 2ed7082..c5f5053 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/Function.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/Function.java @@ -1,4 +1,4 @@ -package net.sourceforge.smallbasic.ioio; +package ioio.smallbasic; import ioio.lib.api.exception.ConnectionLostException; diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/IOIOException.java b/ioio/ioio/src/main/java/ioio/smallbasic/IOIOException.java new file mode 100644 index 0000000..9ef7a8d --- /dev/null +++ b/ioio/ioio/src/main/java/ioio/smallbasic/IOIOException.java @@ -0,0 +1,19 @@ +package ioio.smallbasic; + +public class IOIOException extends RuntimeException { + public IOIOException(Exception exception) { + super(exception.getMessage()); + } + + public IOIOException(String message) { + super(message); + } + + public String getMessage() { + String result = super.getMessage(); + if (result != null) { + result = result.replace("ioio.smallbasic.IOIOException:", "").trim(); + } + return result; + } +} diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOImpl.java b/ioio/ioio/src/main/java/ioio/smallbasic/IOIOImpl.java similarity index 96% rename from ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOImpl.java rename to ioio/ioio/src/main/java/ioio/smallbasic/IOIOImpl.java index 0d56ff8..b633cee 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOImpl.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/IOIOImpl.java @@ -1,4 +1,4 @@ -package net.sourceforge.smallbasic.ioio; +package ioio.smallbasic; import java.io.IOException; diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOLock.java b/ioio/ioio/src/main/java/ioio/smallbasic/IOLock.java similarity index 93% rename from ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOLock.java rename to ioio/ioio/src/main/java/ioio/smallbasic/IOLock.java index d1cdc2c..9d555aa 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOLock.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/IOLock.java @@ -1,4 +1,4 @@ -package net.sourceforge.smallbasic.ioio; +package ioio.smallbasic; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -70,7 +70,7 @@ public void process(I input) { try { consumer.accept(input); } catch (ConnectionLostException | InterruptedException | IncompatibilityException e) { - throw new RuntimeException(e); + throw new IOIOException(e); } consumer = null; } @@ -90,10 +90,10 @@ private CountDownLatch beginLatch() { private void endLatch(CountDownLatch latch) { try { if (!latch.await(TIMEOUT_SECS, TimeUnit.SECONDS)) { - throw new RuntimeException("Timeout waiting for device"); + throw new IOIOException("Timeout waiting for device"); } } catch (InterruptedException e) { - throw new RuntimeException(e); + throw new IOIOException(e); } } } diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOService.java b/ioio/ioio/src/main/java/ioio/smallbasic/IOService.java similarity index 98% rename from ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOService.java rename to ioio/ioio/src/main/java/ioio/smallbasic/IOService.java index 7734df3..e09927f 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOService.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/IOService.java @@ -1,4 +1,4 @@ -package net.sourceforge.smallbasic.ioio; +package ioio.smallbasic; import java.io.IOException; import java.util.ArrayList; diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOTask.java b/ioio/ioio/src/main/java/ioio/smallbasic/IOTask.java similarity index 94% rename from ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOTask.java rename to ioio/ioio/src/main/java/ioio/smallbasic/IOTask.java index 28c3552..ddb5c2c 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOTask.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/IOTask.java @@ -1,4 +1,4 @@ -package net.sourceforge.smallbasic.ioio; +package ioio.smallbasic; import java.io.Closeable; import java.io.IOException; @@ -23,7 +23,7 @@ public void close() { public void handleError() { if (error.get() != null) { - throw new RuntimeException(error.get()); + throw new IOIOException(error.get()); } } diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/PulseInputImpl.java b/ioio/ioio/src/main/java/ioio/smallbasic/PulseInputImpl.java similarity index 97% rename from ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/PulseInputImpl.java rename to ioio/ioio/src/main/java/ioio/smallbasic/PulseInputImpl.java index f106afa..c9d2cf7 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/PulseInputImpl.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/PulseInputImpl.java @@ -1,4 +1,4 @@ -package net.sourceforge.smallbasic.ioio; +package ioio.smallbasic; import ioio.lib.api.IOIO; import ioio.lib.api.PulseInput; diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/PwmOutputImpl.java b/ioio/ioio/src/main/java/ioio/smallbasic/PwmOutputImpl.java similarity index 97% rename from ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/PwmOutputImpl.java rename to ioio/ioio/src/main/java/ioio/smallbasic/PwmOutputImpl.java index e66953f..62e07ad 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/PwmOutputImpl.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/PwmOutputImpl.java @@ -1,4 +1,4 @@ -package net.sourceforge.smallbasic.ioio; +package ioio.smallbasic; import java.io.IOException; import java.util.concurrent.atomic.AtomicReference; diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/SequencerImpl.java b/ioio/ioio/src/main/java/ioio/smallbasic/SequencerImpl.java new file mode 100644 index 0000000..91ac085 --- /dev/null +++ b/ioio/ioio/src/main/java/ioio/smallbasic/SequencerImpl.java @@ -0,0 +1,86 @@ +package ioio.smallbasic; + +import ioio.lib.api.AnalogInput; +import ioio.lib.api.IOIO; +import ioio.lib.api.Sequencer; +import ioio.lib.api.exception.ConnectionLostException; +import ioio.lib.spi.Log; + +public class SequencerImpl extends IOTask implements Sequencer { + private static final String TAG = "Sequencer"; + private final IOLock lock = new IOLock<>(); + private Sequencer input = null; + + public SequencerImpl() { + super(); + Log.i(TAG, "created"); + } + + @Override + public int available() { + return 0; // lock.invokeInt(Sequencer::available); + } + + @Override + public Event getLastEvent() throws ConnectionLostException { + return null; + } + + @Override + public void manualStart(ChannelCue[] cues) throws ConnectionLostException { + + } + + @Override + public void manualStop() throws ConnectionLostException { + + } + + @Override + public void pause() throws ConnectionLostException { + + } + + @Override + public void push(ChannelCue[] cues, int duration) throws + ConnectionLostException, + InterruptedException { + + } + + @Override + public void setEventQueueSize(int size) throws ConnectionLostException { + + } + + @Override + public void start() throws ConnectionLostException { + + } + + @Override + public void stop() throws ConnectionLostException { + + } + + @Override + public Event waitEvent() throws ConnectionLostException, InterruptedException { + return null; + } + + @Override + public void waitEventType(Event.Type type) throws ConnectionLostException, InterruptedException { + + } + + @Override + void loop() throws ConnectionLostException, InterruptedException { + + } + + @Override + void setup(IOIO ioio) throws ConnectionLostException { + Log.i(TAG, "setup entered"); + //input = ioio.openSequencer(pin); + } +} diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/SpiMasterImpl.java b/ioio/ioio/src/main/java/ioio/smallbasic/SpiMasterImpl.java similarity index 98% rename from ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/SpiMasterImpl.java rename to ioio/ioio/src/main/java/ioio/smallbasic/SpiMasterImpl.java index 88ccc01..ec74e05 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/SpiMasterImpl.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/SpiMasterImpl.java @@ -1,4 +1,4 @@ -package net.sourceforge.smallbasic.ioio; +package ioio.smallbasic; import java.io.IOException; diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TimerUtil.java b/ioio/ioio/src/main/java/ioio/smallbasic/TimerUtil.java similarity index 92% rename from ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TimerUtil.java rename to ioio/ioio/src/main/java/ioio/smallbasic/TimerUtil.java index 74d77df..4689f33 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TimerUtil.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/TimerUtil.java @@ -1,4 +1,4 @@ -package net.sourceforge.smallbasic.ioio; +package ioio.smallbasic; public class TimerUtil { private static int latency = 50; diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java b/ioio/ioio/src/main/java/ioio/smallbasic/TwiMasterImpl.java similarity index 97% rename from ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java rename to ioio/ioio/src/main/java/ioio/smallbasic/TwiMasterImpl.java index cc6ee1a..8fe107a 100644 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/TwiMasterImpl.java @@ -1,4 +1,4 @@ -package net.sourceforge.smallbasic.ioio; +package ioio.smallbasic; import java.io.IOException; diff --git a/ioio/ioio/src/main/java/ioio/lib/android/AccessoryConnectionBootstrap.java b/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryConnectionBootstrap.java similarity index 87% rename from ioio/ioio/src/main/java/ioio/lib/android/AccessoryConnectionBootstrap.java rename to ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryConnectionBootstrap.java index fb174f1..2ff9117 100644 --- a/ioio/ioio/src/main/java/ioio/lib/android/AccessoryConnectionBootstrap.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryConnectionBootstrap.java @@ -27,17 +27,12 @@ * or implied. */ -package ioio.lib.android; +package ioio.smallbasic.android; -import android.annotation.TargetApi; import android.app.PendingIntent; -import android.content.BroadcastReceiver; import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; import android.hardware.usb.UsbAccessory; import android.hardware.usb.UsbManager; -import android.os.Build; import android.os.ParcelFileDescriptor; import java.io.BufferedOutputStream; @@ -57,7 +52,8 @@ import ioio.lib.spi.Log; import ioio.lib.spi.NoRuntimeSupportException; -public class AccessoryConnectionBootstrap extends BroadcastReceiver implements IOIOConnectionBootstrap, IOIOConnectionFactory { +public class AccessoryConnectionBootstrap //extends BroadcastReceiver + implements IOIOConnectionBootstrap, IOIOConnectionFactory { private static final String TAG = AccessoryConnectionBootstrap.class.getSimpleName(); private static final String ACTION_USB_PERMISSION = "ioio.lib.accessory.action.USB_PERMISSION"; private static final String EXTRA_PERMISSION_GRANTED = UsbManager.EXTRA_PERMISSION_GRANTED; @@ -78,7 +74,7 @@ public AccessoryConnectionBootstrap() throws NoRuntimeSupportException { Log.d(TAG, "creating AccessoryConnectionBootstrap"); this.activity = IOIOLoader.getContext(); this.usbManager = (UsbManager) activity.getSystemService(Context.USB_SERVICE); - registerReceiver(); + // registerReceiver(); } @Override @@ -101,23 +97,23 @@ public String getType() { return Connection.class.getCanonicalName(); } - //@Override - public void onDestroy() { - activity.unregisterReceiver(this); - } - - @Override - public synchronized void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - if (ACTION_USB_PERMISSION.equals(action)) { - pendingIntent = null; - if (intent.getBooleanExtra(EXTRA_PERMISSION_GRANTED, false)) { - notifyAll(); - } else { - Log.e(TAG, "Permission denied"); - } - } - } +// //@Override +// public void onDestroy() { +// activity.unregisterReceiver(this); +// } + +// @Override +// public synchronized void onReceive(Context context, Intent intent) { +// final String action = intent.getAction(); +// if (ACTION_USB_PERMISSION.equals(action)) { +// pendingIntent = null; +// if (intent.getBooleanExtra(EXTRA_PERMISSION_GRANTED, false)) { +// notifyAll(); +// } else { +// Log.e(TAG, "Permission denied"); +// } +// } +// } //@Override public synchronized void open() { @@ -160,13 +156,13 @@ private void forceWait() { } } - @TargetApi(Build.VERSION_CODES.TIRAMISU) - private void registerReceiver() { - IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - activity.registerReceiver(this, filter, Context.RECEIVER_NOT_EXPORTED); - } - } +// @TargetApi(Build.VERSION_CODES.TIRAMISU) +// private void registerReceiver() { +// IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION); +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { +// activity.registerReceiver(this, filter, Context.RECEIVER_NOT_EXPORTED); +// } +// } private boolean tryOpen() { // Find the accessory. @@ -180,11 +176,11 @@ private boolean tryOpen() { // Check for permission to access the accessory. if (!usbManager.hasPermission(accessory)) { - if (pendingIntent == null) { - Log.v(TAG, "Requesting permission."); - pendingIntent = PendingIntent.getBroadcast(activity, 0, new Intent(ACTION_USB_PERMISSION), 0); - usbManager.requestPermission(accessory, pendingIntent); - } +// if (pendingIntent == null) { +// Log.v(TAG, "Requesting permission."); +// pendingIntent = PendingIntent.getBroadcast(activity, 0, new Intent(ACTION_USB_PERMISSION), PendingIntent.FLAG_IMMUTABLE); +// usbManager.requestPermission(accessory, pendingIntent); +// } return false; } diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionManager.java b/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionManager.java new file mode 100644 index 0000000..51bea0f --- /dev/null +++ b/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionManager.java @@ -0,0 +1,87 @@ +package ioio.smallbasic.android; + +import android.annotation.TargetApi; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.hardware.usb.UsbAccessory; +import android.hardware.usb.UsbManager; +import android.os.Build; + +import ioio.smallbasic.IOIOException; + +import java.util.concurrent.CountDownLatch; + +import ioio.lib.spi.Log; + +public class AccessoryPermissionManager extends BroadcastReceiver { + private static final String TAG = AccessoryPermissionManager.class.getSimpleName(); + private static final String ACTION_USB_PERMISSION = "ioio.lib.accessory.action.USB_PERMISSION"; + private static final String EXTRA_PERMISSION_GRANTED = UsbManager.EXTRA_PERMISSION_GRANTED; + + private final UsbManager usbManager; + private final Context context; + private final CountDownLatch latch; + private boolean hasPermission; + + public AccessoryPermissionManager() throws InterruptedException { + Log.d(TAG, "validating accessory permission"); + this.context = IOIOLoader.getContext(); + this.usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE); + this.latch = new CountDownLatch(1); + this.hasPermission = false; + debug("in validate"); + validate(); + debug("done validate"); + } + + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (ACTION_USB_PERMISSION.equals(action)) { + hasPermission = intent.getBooleanExtra(EXTRA_PERMISSION_GRANTED, false); + debug("has perm " + hasPermission) ; + Log.v(TAG, "Permission: " + (hasPermission ? "granted" : "denied")); + context.unregisterReceiver(this); +// latch.countDown(); + } + } + + @TargetApi(Build.VERSION_CODES.TIRAMISU) + private void validate() throws InterruptedException { + UsbAccessory[] accessories = usbManager.getAccessoryList(); + UsbAccessory accessory = (accessories == null ? null : accessories[0]); + if (accessory == null) { + throw new IOIOException("No accessory found."); + } + if (usbManager.hasPermission(accessory)) { + debug("already granted"); + Log.v(TAG, "Permission already granted."); + } else { + Log.v(TAG, "Requesting permission."); + IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION); + context.registerReceiver(this, filter, Context.RECEIVER_NOT_EXPORTED); + int flags = PendingIntent.FLAG_IMMUTABLE; + Intent intent = new Intent(ACTION_USB_PERMISSION); + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, flags); + usbManager.requestPermission(accessory, pendingIntent); + debug("about to wait"); + latch.await(); + debug("done waiting"); + if (!hasPermission) { + throw new IOIOException("Permission denied."); + } + } + } + + private void debug(String message) { +// ((Activity)context).runOnUiThread(new Runnable() { +// @Override +// public void run() { +// Toast.makeText(context, message.trim(), Toast.LENGTH_LONG).show(); +// } +// }); + } +} diff --git a/ioio/ioio/src/main/java/ioio/lib/android/AndroidLogger.java b/ioio/ioio/src/main/java/ioio/smallbasic/android/AndroidLogger.java similarity index 88% rename from ioio/ioio/src/main/java/ioio/lib/android/AndroidLogger.java rename to ioio/ioio/src/main/java/ioio/smallbasic/android/AndroidLogger.java index c8a8b0e..d99d2a2 100644 --- a/ioio/ioio/src/main/java/ioio/lib/android/AndroidLogger.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/android/AndroidLogger.java @@ -1,4 +1,4 @@ -package ioio.lib.android; +package ioio.smallbasic.android; import android.util.Log; diff --git a/ioio/ioio/src/main/java/ioio/lib/android/IOIOLoader.java b/ioio/ioio/src/main/java/ioio/smallbasic/android/IOIOLoader.java similarity index 94% rename from ioio/ioio/src/main/java/ioio/lib/android/IOIOLoader.java rename to ioio/ioio/src/main/java/ioio/smallbasic/android/IOIOLoader.java index df0bdf1..5233878 100644 --- a/ioio/ioio/src/main/java/ioio/lib/android/IOIOLoader.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/android/IOIOLoader.java @@ -1,4 +1,4 @@ -package ioio.lib.android; +package ioio.smallbasic.android; import android.content.Context; diff --git a/ioio/ioio/src/main/java/ioio/lib/pc/SerialPortIOIOConnection.java b/ioio/ioio/src/main/java/ioio/smallbasic/pc/SerialPortIOIOConnection.java similarity index 98% rename from ioio/ioio/src/main/java/ioio/lib/pc/SerialPortIOIOConnection.java rename to ioio/ioio/src/main/java/ioio/smallbasic/pc/SerialPortIOIOConnection.java index dd4d151..baf6dcd 100644 --- a/ioio/ioio/src/main/java/ioio/lib/pc/SerialPortIOIOConnection.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/pc/SerialPortIOIOConnection.java @@ -1,4 +1,4 @@ -package ioio.lib.pc; +package ioio.smallbasic.pc; import com.fazecast.jSerialComm.SerialPort; import ioio.lib.api.IOIOConnection; diff --git a/ioio/ioio/src/main/java/ioio/lib/pc/SerialPortIOIOConnectionBootstrap.java b/ioio/ioio/src/main/java/ioio/smallbasic/pc/SerialPortIOIOConnectionBootstrap.java similarity index 99% rename from ioio/ioio/src/main/java/ioio/lib/pc/SerialPortIOIOConnectionBootstrap.java rename to ioio/ioio/src/main/java/ioio/smallbasic/pc/SerialPortIOIOConnectionBootstrap.java index 026625a..eaec883 100644 --- a/ioio/ioio/src/main/java/ioio/lib/pc/SerialPortIOIOConnectionBootstrap.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/pc/SerialPortIOIOConnectionBootstrap.java @@ -27,7 +27,7 @@ * or implied. */ -package ioio.lib.pc; +package ioio.smallbasic.pc; import com.fazecast.jSerialComm.SerialPort; import ioio.lib.api.IOIOConnection; diff --git a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/ConnectionController.java b/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/ConnectionController.java deleted file mode 100644 index 2ae741a..0000000 --- a/ioio/ioio/src/main/java/net/sourceforge/smallbasic/ioio/ConnectionController.java +++ /dev/null @@ -1,41 +0,0 @@ -package net.sourceforge.smallbasic.ioio; - -import ioio.lib.util.IOIOBaseApplicationHelper; -import ioio.lib.util.IOIOConnectionManager; -import ioio.lib.util.IOIOConnectionRegistry; -import ioio.lib.util.IOIOLooperProvider; - -public class ConnectionController extends IOIOBaseApplicationHelper { - private final IOIOConnectionManager manager = new IOIOConnectionManager(this); - - static { - if (getIsRunningOnAndroid()) { - IOIOConnectionRegistry.addBootstraps(new String[]{"ioio.lib.android.AccessoryConnectionBootstrap"}); - } else { - IOIOConnectionRegistry.addBootstraps(new String[]{"ioio.lib.pc.SerialPortIOIOConnectionBootstrap"}); - } - } - - public ConnectionController(IOIOLooperProvider provider) { - super(provider); - } - - public void start() { - manager.start(); - } - - public void stop() { - manager.stop(); - } - - private static boolean getIsRunningOnAndroid() { - boolean result; - try { - Class.forName("android.os.Build"); - result = true; - } catch (ClassNotFoundException e) { - result = false; - } - return result; - } -} diff --git a/ioio/ioio/src/test/java/net/sourceforge/smallbasic/ioio/AnalogInputTest.java b/ioio/ioio/src/test/java/ioio/smallbasic/AnalogInputTest.java similarity index 87% rename from ioio/ioio/src/test/java/net/sourceforge/smallbasic/ioio/AnalogInputTest.java rename to ioio/ioio/src/test/java/ioio/smallbasic/AnalogInputTest.java index 02f8a45..f1dd839 100644 --- a/ioio/ioio/src/test/java/net/sourceforge/smallbasic/ioio/AnalogInputTest.java +++ b/ioio/ioio/src/test/java/ioio/smallbasic/AnalogInputTest.java @@ -1,4 +1,4 @@ -package net.sourceforge.smallbasic.ioio; +package ioio.smallbasic; import java.io.IOException; diff --git a/ioio/ioio/src/test/java/net/sourceforge/smallbasic/ioio/DigitalOutputTest.java b/ioio/ioio/src/test/java/ioio/smallbasic/DigitalOutputTest.java similarity index 85% rename from ioio/ioio/src/test/java/net/sourceforge/smallbasic/ioio/DigitalOutputTest.java rename to ioio/ioio/src/test/java/ioio/smallbasic/DigitalOutputTest.java index d4bc098..fd956e9 100644 --- a/ioio/ioio/src/test/java/net/sourceforge/smallbasic/ioio/DigitalOutputTest.java +++ b/ioio/ioio/src/test/java/ioio/smallbasic/DigitalOutputTest.java @@ -1,9 +1,11 @@ -package net.sourceforge.smallbasic.ioio; +package ioio.smallbasic; import java.io.IOException; import ioio.lib.api.IOIO; import ioio.lib.api.exception.ConnectionLostException; +import ioio.smallbasic.DigitalOutputImpl; +import ioio.smallbasic.IOService; class DigitalOutputTest { public static void main(String[] args) throws InterruptedException, ConnectionLostException, IOException { diff --git a/ioio/main.cpp b/ioio/main.cpp index aca3fa6..012b1a4 100644 --- a/ioio/main.cpp +++ b/ioio/main.cpp @@ -33,15 +33,15 @@ jobject g_activity; #define detachCurrentThread() #endif -#define CLASS_ANALOGINPUT "net/sourceforge/smallbasic/ioio/AnalogInputImpl" -#define CLASS_DIGITALINPUT "net/sourceforge/smallbasic/ioio/DigitalInputImpl" -#define CLASS_DIGITALOUTPUT "net/sourceforge/smallbasic/ioio/DigitalOutputImpl" -#define CLASS_PULSEINPUT "net/sourceforge/smallbasic/ioio/PulseInputImpl" -#define CLASS_PWMOUTPUT "net/sourceforge/smallbasic/ioio/PwmOutputImpl" -#define CLASS_CAPSENSE "net/sourceforge/smallbasic/ioio/CapsenseImpl" -#define CLASS_TWIMASTER "net/sourceforge/smallbasic/ioio/TwiMasterImpl" -#define CLASS_SPIMASTER "net/sourceforge/smallbasic/ioio/SpiMasterImpl" -#define CLASS_IOIO "net/sourceforge/smallbasic/ioio/IOIOImpl" +#define CLASS_ANALOGINPUT "ioio/smallbasic/AnalogInputImpl" +#define CLASS_DIGITALINPUT "ioio/smallbasic/DigitalInputImpl" +#define CLASS_DIGITALOUTPUT "ioio/smallbasic/DigitalOutputImpl" +#define CLASS_PULSEINPUT "ioio/smallbasic/PulseInputImpl" +#define CLASS_PWMOUTPUT "ioio/smallbasic/PwmOutputImpl" +#define CLASS_CAPSENSE "ioio/smallbasic/CapsenseImpl" +#define CLASS_TWIMASTER "ioio/smallbasic/TwiMasterImpl" +#define CLASS_SPIMASTER "ioio/smallbasic/SpiMasterImpl" +#define CLASS_IOIO "ioio/smallbasic/IOIOImpl" #define CLASS_IOTASK_ID 1 #define ARRAY_SIZE 10 @@ -135,8 +135,8 @@ struct IOTask { if (exc) { if (retval) { g_env->ExceptionClear(); - jclass clazz = g_env->FindClass("java/lang/Object"); - jmethodID methodId = g_env->GetMethodID(clazz, "toString", "()Ljava/lang/String;"); + jclass clazz = g_env->FindClass("java/lang/Throwable"); + jmethodID methodId = g_env->GetMethodID(clazz, "getMessage", "()Ljava/lang/String;"); jstring jstr = (jstring) g_env->CallObjectMethod(exc, methodId); const char *message = g_env->GetStringUTFChars(jstr, JNI_FALSE); error(retval, message); @@ -548,7 +548,7 @@ extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void* reserved) { // // Retrieves the _app->activity->clazz value sent from App/JNI to Java to IOIOLoader // -extern "C" JNIEXPORT void JNICALL Java_net_sourceforge_smallbasic_ioio_IOIOLoader_init +extern "C" JNIEXPORT void JNICALL Java_ioio_smallbasic_android_IOIOLoader_init (JNIEnv *env, jclass clazz, jobject activity) { logEntered(); jclass longClass = env->FindClass("java/lang/Long"); @@ -566,7 +566,7 @@ SBLIB_API void sblib_free(int cls_id, int id) { if (id != -1) { switch (cls_id) { case CLASS_IOTASK_ID: - if (id != -1 && g_ioTaskMap.find(id) != g_ioTaskMap.end()) { + if (g_ioTaskMap.find(id) != g_ioTaskMap.end()) { g_ioTaskMap.at(id).invokeVoidVoid("close", nullptr); g_ioTaskMap.erase(id); } From 3dab3eac167ce16e164245b9bb11544011f9fd34 Mon Sep 17 00:00:00 2001 From: chrisws Date: Fri, 29 Mar 2024 08:05:11 +1030 Subject: [PATCH 027/131] IOIO: update USB permission handling --- ioio/ioio/src/main/AndroidManifest.xml | 11 +- .../ioio/smallbasic/ConnectionController.java | 22 +- .../android/AccessoryConnectionBootstrap.java | 265 +----------------- .../android/AccessoryPermissionCheck.java | 85 ++++++ .../android/AccessoryPermissionManager.java | 87 ------ .../android/BluetoothConnection.java | 155 ++++++++++ .../pc/SerialPortIOIOConnection.java | 7 +- .../pc/SerialPortIOIOConnectionBootstrap.java | 9 +- ioio/ioio/src/main/res/xml/device_filter.xml | 4 + 9 files changed, 284 insertions(+), 361 deletions(-) create mode 100644 ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionCheck.java delete mode 100644 ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionManager.java create mode 100644 ioio/ioio/src/main/java/ioio/smallbasic/android/BluetoothConnection.java create mode 100644 ioio/ioio/src/main/res/xml/device_filter.xml diff --git a/ioio/ioio/src/main/AndroidManifest.xml b/ioio/ioio/src/main/AndroidManifest.xml index 1b7c589..68ac02c 100644 --- a/ioio/ioio/src/main/AndroidManifest.xml +++ b/ioio/ioio/src/main/AndroidManifest.xml @@ -1,4 +1,13 @@ - + + + + + + + + + + diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/ConnectionController.java b/ioio/ioio/src/main/java/ioio/smallbasic/ConnectionController.java index 0fd2e22..68690de 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/ConnectionController.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/ConnectionController.java @@ -8,16 +8,16 @@ public class ConnectionController extends IOIOBaseApplicationHelper { private final IOIOConnectionManager manager = new IOIOConnectionManager(this); private static final boolean isAndroid; - private static final String ANDROID_BOOTSTRAP = "ioio.smallbasic.android.AccessoryConnectionBootstrap"; - private static final String PERMISSION_MANAGER = "ioio.smallbasic.android.AccessoryPermissionManager"; - private static final String DESKTOP_BOOTSTRAP = "ioio.smallbasic.pc.SerialPortIOIOConnectionBootstrap"; + private static final String ACCESSORY_BOOTSTRAP = "ioio.smallbasic.android.AccessoryConnectionBootstrap"; + private static final String SERIAL_PORT_BOOTSTRAP = "ioio.smallbasic.pc.SerialPortIOIOConnectionBootstrap"; + private static final String PERMISSION_CHECK = "ioio.smallbasic.android.AccessoryPermissionCheck"; static { isAndroid = getIsAndroidBuild(); if (isAndroid) { - IOIOConnectionRegistry.addBootstraps(new String[] { ANDROID_BOOTSTRAP }); + IOIOConnectionRegistry.addBootstraps(new String[] { ACCESSORY_BOOTSTRAP }); } else { - IOIOConnectionRegistry.addBootstraps(new String[] { DESKTOP_BOOTSTRAP }); + IOIOConnectionRegistry.addBootstraps(new String[] { SERIAL_PORT_BOOTSTRAP }); } } @@ -27,9 +27,9 @@ public ConnectionController(IOIOLooperProvider provider) { public void start() { if (isAndroid) { - validateAccessoryPermission(); + permitAccessory(); } - //manager.start(); + manager.start(); } public void stop() { @@ -39,7 +39,7 @@ public void stop() { private static boolean getIsAndroidBuild() { boolean result; try { - Class.forName(ANDROID_BOOTSTRAP); + Class.forName(ACCESSORY_BOOTSTRAP); result = true; } catch (ClassNotFoundException e) { result = false; @@ -47,12 +47,12 @@ private static boolean getIsAndroidBuild() { return result; } - private static void validateAccessoryPermission() { + private static void permitAccessory() { try { - Class.forName(PERMISSION_MANAGER).newInstance(); + Class.forName(PERMISSION_CHECK).newInstance(); } catch (Exception e) { - throw new IOIOException(e.toString()); + throw new IOIOException(e); } } } diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryConnectionBootstrap.java b/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryConnectionBootstrap.java index 2ff9117..23c11c7 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryConnectionBootstrap.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryConnectionBootstrap.java @@ -29,57 +29,32 @@ package ioio.smallbasic.android; -import android.app.PendingIntent; import android.content.Context; import android.hardware.usb.UsbAccessory; import android.hardware.usb.UsbManager; -import android.os.ParcelFileDescriptor; -import java.io.BufferedOutputStream; -import java.io.FileDescriptor; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; import java.util.Collection; import ioio.lib.api.IOIOConnection; -import ioio.lib.api.exception.ConnectionLostException; -import ioio.lib.impl.FixedReadBufferedInputStream; import ioio.lib.spi.IOIOConnectionBootstrap; import ioio.lib.spi.IOIOConnectionFactory; import ioio.lib.spi.Log; import ioio.lib.spi.NoRuntimeSupportException; -public class AccessoryConnectionBootstrap //extends BroadcastReceiver - implements IOIOConnectionBootstrap, IOIOConnectionFactory { +public class AccessoryConnectionBootstrap implements IOIOConnectionBootstrap, IOIOConnectionFactory { private static final String TAG = AccessoryConnectionBootstrap.class.getSimpleName(); - private static final String ACTION_USB_PERMISSION = "ioio.lib.accessory.action.USB_PERMISSION"; - private static final String EXTRA_PERMISSION_GRANTED = UsbManager.EXTRA_PERMISSION_GRANTED; - - private final Context activity; - private final UsbManager usbManager; - private boolean shouldTryOpen = false; - private PendingIntent pendingIntent; - private ParcelFileDescriptor fileDescriptor; - private InputStream inputStream; - private OutputStream outputStream; - - private enum InstanceState { - INIT, CONNECTED, DEAD - } public AccessoryConnectionBootstrap() throws NoRuntimeSupportException { Log.d(TAG, "creating AccessoryConnectionBootstrap"); - this.activity = IOIOLoader.getContext(); - this.usbManager = (UsbManager) activity.getSystemService(Context.USB_SERVICE); - // registerReceiver(); } @Override public IOIOConnection createConnection() { - return new Connection(); + Log.i(TAG, "createConnection"); + Context activity = IOIOLoader.getContext(); + UsbManager usbManager = (UsbManager) activity.getSystemService(Context.USB_SERVICE); + UsbAccessory accessory = usbManager.getAccessoryList()[0]; + return new BluetoothConnection(getUsbManager().openAccessory(accessory)); } @Override @@ -94,231 +69,11 @@ public void getFactories(Collection result) { @Override public String getType() { - return Connection.class.getCanonicalName(); - } - -// //@Override -// public void onDestroy() { -// activity.unregisterReceiver(this); -// } - -// @Override -// public synchronized void onReceive(Context context, Intent intent) { -// final String action = intent.getAction(); -// if (ACTION_USB_PERMISSION.equals(action)) { -// pendingIntent = null; -// if (intent.getBooleanExtra(EXTRA_PERMISSION_GRANTED, false)) { -// notifyAll(); -// } else { -// Log.e(TAG, "Permission denied"); -// } -// } -// } - - //@Override - public synchronized void open() { - notifyAll(); + return BluetoothConnection.class.getCanonicalName(); } - //@Override - public synchronized void reopen() { - notifyAll(); - } - - private synchronized void disconnect() { - // This should abort any current open attempt. - Log.d(TAG, "private disconnect"); - shouldTryOpen = false; - notifyAll(); - - // And this should kill any ongoing connections. - if (fileDescriptor != null) { - try { - fileDescriptor.close(); - } catch (IOException e) { - Log.e(TAG, "Failed to close file descriptor.", e); - } - fileDescriptor = null; - } - - if (pendingIntent != null) { - pendingIntent.cancel(); - pendingIntent = null; - } - Log.d(TAG, "leaving private disconnect"); - } - - private void forceWait() { - try { - wait(); - } catch (InterruptedException e) { - Log.e(TAG, "Do not interrupt me!"); - } - } - -// @TargetApi(Build.VERSION_CODES.TIRAMISU) -// private void registerReceiver() { -// IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION); -// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { -// activity.registerReceiver(this, filter, Context.RECEIVER_NOT_EXPORTED); -// } -// } - - private boolean tryOpen() { - // Find the accessory. - UsbAccessory[] accessories = usbManager.getAccessoryList(); - UsbAccessory accessory = (accessories == null ? null : accessories[0]); - - if (accessory == null) { - Log.v(TAG, "No accessory found."); - return false; - } - - // Check for permission to access the accessory. - if (!usbManager.hasPermission(accessory)) { -// if (pendingIntent == null) { -// Log.v(TAG, "Requesting permission."); -// pendingIntent = PendingIntent.getBroadcast(activity, 0, new Intent(ACTION_USB_PERMISSION), PendingIntent.FLAG_IMMUTABLE); -// usbManager.requestPermission(accessory, pendingIntent); -// } - return false; - } - - boolean success = false; - - // From this point on, if anything goes wrong, we're responsible for canceling the intent. - try { - // Obtain a file descriptor. - fileDescriptor = usbManager.openAccessory(accessory); - if (fileDescriptor == null) { - Log.v(TAG, "Failed to open file descriptor."); - return false; - } - - // From this point on, if anything goes wrong, we're responsible for closing the file - // descriptor. - try { - FileDescriptor fd = fileDescriptor.getFileDescriptor(); - // Apparently, some Android devices (e.g. Nexus 5) only support read operations of - // multiples of the endpoint buffer size. So there you have it! - inputStream = new FixedReadBufferedInputStream(new FileInputStream(fd), 1024); - outputStream = new BufferedOutputStream(new FileOutputStream(fd), 1024); - - // Soft-open the connection - outputStream.write(0x00); - outputStream.flush(); - - // We're going to block now. We're counting on the IOIO to - // write back a byte, or otherwise we're locked until - // physical disconnection. This is a known OpenAccessory - // bug: - // http://code.google.com/p/android/issues/detail?id=20545 - while (inputStream.read() != 1) { - trySleep(); - } - - success = true; - return true; - } catch (IOException e) { - Log.v(TAG, "Failed to open streams", e); - return false; - } finally { - if (!success) { - try { - fileDescriptor.close(); - } catch (IOException e) { - Log.e(TAG, "Failed to close file descriptor.", e); - } - fileDescriptor = null; - } - } - } finally { - if (!success && pendingIntent != null) { - pendingIntent.cancel(); - pendingIntent = null; - } - } - } - - private void trySleep() { - synchronized (AccessoryConnectionBootstrap.this) { - try { - AccessoryConnectionBootstrap.this.wait(1000); - } catch (InterruptedException e) { - Log.e(TAG, e.toString()); - } - } - } - - private synchronized void waitForConnect(Connection connection) throws ConnectionLostException { - // In order to simplify the connection process in face of many different sequences of events - // that might occur, we collapsed the entire sequence into one non-blocking method, - // tryOpen(), which tries the entire process from the beginning, undoes everything if - // something along the way fails and always returns immediately. - // This method, simply calls tryOpen() in a loop until it succeeds or until we're no longer - // interested. Between attempts, it waits until "something interesting" has happened, which - // may be permission granted, the client telling us to try again (via reopen()) or stop - // trying, etc. - shouldTryOpen = true; - while (shouldTryOpen) { - if (tryOpen()) { - // Success! - return; - } - forceWait(); - } - throw new ConnectionLostException(); - } - - private class Connection implements IOIOConnection { - private InstanceState instanceState_ = InstanceState.INIT; - - @Override - public boolean canClose() { - return false; - } - - @Override - public void disconnect() { - Log.d(TAG, "disconnect"); - synchronized(AccessoryConnectionBootstrap.this) { - if (instanceState_ != InstanceState.DEAD) { - AccessoryConnectionBootstrap.this.disconnect(); - instanceState_ = InstanceState.DEAD; - } - } - } - - @Override - public InputStream getInputStream() { - return inputStream; - } - - @Override - public OutputStream getOutputStream() { - return outputStream; - } - - @Override - public void waitForConnect() throws ConnectionLostException { - synchronized(AccessoryConnectionBootstrap.this) { - if (instanceState_ != InstanceState.INIT) { - throw new IllegalStateException("waitForConnect() may only be called once"); - } - - try { - AccessoryConnectionBootstrap.this.waitForConnect(this); - instanceState_ = InstanceState.CONNECTED; - } catch (ConnectionLostException e) { - instanceState_ = InstanceState.DEAD; - throw e; - } - } - } - - @Override - protected void finalize() { - disconnect(); - } + private UsbManager getUsbManager() { + Context activity = IOIOLoader.getContext(); + return (UsbManager) activity.getSystemService(Context.USB_SERVICE); } } diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionCheck.java b/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionCheck.java new file mode 100644 index 0000000..e55a07d --- /dev/null +++ b/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionCheck.java @@ -0,0 +1,85 @@ +package ioio.smallbasic.android; + +import android.annotation.TargetApi; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.hardware.usb.UsbAccessory; +import android.hardware.usb.UsbManager; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import ioio.lib.spi.Log; +import ioio.smallbasic.IOIOException; + +public class AccessoryPermissionCheck extends BroadcastReceiver { + private static final String TAG = AccessoryPermissionCheck.class.getSimpleName(); + private static final String ACTION_USB_PERMISSION = "android.hardware.usb.action.USB_PERMISSION"; + private static final String PERMISSION_ERROR = "Not permitted to use usb accessory."; + private static final long TIMEOUT_SECS = 30; + private final CountDownLatch latch; + private final AtomicBoolean permitted; + + @TargetApi(Build.VERSION_CODES.TIRAMISU) + public AccessoryPermissionCheck() { + Log.d(TAG, "AccessoryPermissionCheck entered"); + this.permitted = new AtomicBoolean(false); + this.latch = new CountDownLatch(1); + + UsbManager usbManager = (UsbManager) IOIOLoader.getContext().getSystemService(Context.USB_SERVICE); + UsbAccessory[] accessories = usbManager.getAccessoryList(); + UsbAccessory accessory = (accessories == null ? null : accessories[0]); + if (accessory == null) { + throw new IOIOException("No usb accessory found."); + } + if (!usbManager.hasPermission(accessory)) { + new Handler(Looper.getMainLooper()).post(() -> { + Context context = IOIOLoader.getContext(); + IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION); + context.registerReceiver(this, filter, Context.RECEIVER_EXPORTED); + int flags = PendingIntent.FLAG_IMMUTABLE; + Intent intent = new Intent(ACTION_USB_PERMISSION); + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, flags); + usbManager.requestPermission(accessory, pendingIntent); + }); + Log.d(TAG, "waiting for permission"); + try { + if (!latch.await(TIMEOUT_SECS, TimeUnit.SECONDS)) { + permissionError(); + } else { + IOIOLoader.getContext().unregisterReceiver(this); + } + } + catch (InterruptedException e) { + permissionError(); + throw new IOIOException(PERMISSION_ERROR); + } + if (!permitted.get()) { + permissionError(); + } + } + } + + @Override + public void onReceive(Context context, Intent intent) { + Log.d(TAG, intent.getAction()); + synchronized (this) { + if (ACTION_USB_PERMISSION.equals(intent.getAction())) { + permitted.set(intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)); + latch.countDown(); + } + } + } + + private void permissionError() { + IOIOLoader.getContext().unregisterReceiver(this); + throw new IOIOException(PERMISSION_ERROR); + } +} diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionManager.java b/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionManager.java deleted file mode 100644 index 51bea0f..0000000 --- a/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionManager.java +++ /dev/null @@ -1,87 +0,0 @@ -package ioio.smallbasic.android; - -import android.annotation.TargetApi; -import android.app.PendingIntent; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.hardware.usb.UsbAccessory; -import android.hardware.usb.UsbManager; -import android.os.Build; - -import ioio.smallbasic.IOIOException; - -import java.util.concurrent.CountDownLatch; - -import ioio.lib.spi.Log; - -public class AccessoryPermissionManager extends BroadcastReceiver { - private static final String TAG = AccessoryPermissionManager.class.getSimpleName(); - private static final String ACTION_USB_PERMISSION = "ioio.lib.accessory.action.USB_PERMISSION"; - private static final String EXTRA_PERMISSION_GRANTED = UsbManager.EXTRA_PERMISSION_GRANTED; - - private final UsbManager usbManager; - private final Context context; - private final CountDownLatch latch; - private boolean hasPermission; - - public AccessoryPermissionManager() throws InterruptedException { - Log.d(TAG, "validating accessory permission"); - this.context = IOIOLoader.getContext(); - this.usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE); - this.latch = new CountDownLatch(1); - this.hasPermission = false; - debug("in validate"); - validate(); - debug("done validate"); - } - - @Override - public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - if (ACTION_USB_PERMISSION.equals(action)) { - hasPermission = intent.getBooleanExtra(EXTRA_PERMISSION_GRANTED, false); - debug("has perm " + hasPermission) ; - Log.v(TAG, "Permission: " + (hasPermission ? "granted" : "denied")); - context.unregisterReceiver(this); -// latch.countDown(); - } - } - - @TargetApi(Build.VERSION_CODES.TIRAMISU) - private void validate() throws InterruptedException { - UsbAccessory[] accessories = usbManager.getAccessoryList(); - UsbAccessory accessory = (accessories == null ? null : accessories[0]); - if (accessory == null) { - throw new IOIOException("No accessory found."); - } - if (usbManager.hasPermission(accessory)) { - debug("already granted"); - Log.v(TAG, "Permission already granted."); - } else { - Log.v(TAG, "Requesting permission."); - IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION); - context.registerReceiver(this, filter, Context.RECEIVER_NOT_EXPORTED); - int flags = PendingIntent.FLAG_IMMUTABLE; - Intent intent = new Intent(ACTION_USB_PERMISSION); - PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, flags); - usbManager.requestPermission(accessory, pendingIntent); - debug("about to wait"); - latch.await(); - debug("done waiting"); - if (!hasPermission) { - throw new IOIOException("Permission denied."); - } - } - } - - private void debug(String message) { -// ((Activity)context).runOnUiThread(new Runnable() { -// @Override -// public void run() { -// Toast.makeText(context, message.trim(), Toast.LENGTH_LONG).show(); -// } -// }); - } -} diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/android/BluetoothConnection.java b/ioio/ioio/src/main/java/ioio/smallbasic/android/BluetoothConnection.java new file mode 100644 index 0000000..c82c101 --- /dev/null +++ b/ioio/ioio/src/main/java/ioio/smallbasic/android/BluetoothConnection.java @@ -0,0 +1,155 @@ +/* + * Copyright 2015 Ytai Ben-Tsvi. All rights reserved. + * + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARSHAN POURSOHI OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied. + */ + +package ioio.smallbasic.android; + +import android.os.ParcelFileDescriptor; + +import java.io.BufferedOutputStream; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import ioio.lib.api.IOIOConnection; +import ioio.lib.api.exception.ConnectionLostException; +import ioio.lib.impl.FixedReadBufferedInputStream; +import ioio.lib.spi.Log; +import ioio.smallbasic.IOIOException; + +class BluetoothConnection implements IOIOConnection { + private static final String TAG = BluetoothConnection.class.getSimpleName(); + private ConnectionState state; + private InputStream inputStream; + private OutputStream outputStream; + private ParcelFileDescriptor fileDescriptor; + + private enum ConnectionState { + INIT, CONNECTED, DISCONNECTED + } + + BluetoothConnection(ParcelFileDescriptor fileDescriptor) { + Log.i(TAG, "creating BluetoothConnection"); + this.fileDescriptor = fileDescriptor; + this.state = ConnectionState.INIT; + this.inputStream = null; + this.outputStream = null; + if (this.fileDescriptor == null) { + throw new IOIOException("Failed to obtain descriptor"); + } + } + + @Override + public boolean canClose() { + return false; + } + + @Override + public synchronized void disconnect() { + Log.i(TAG, "disconnect entered"); + this.state = ConnectionState.DISCONNECTED; + if (fileDescriptor != null) { + try { + fileDescriptor.close(); + } catch (IOException e) { + Log.e(TAG, "Failed to close file descriptor.", e); + } + fileDescriptor = null; + } + Log.d(TAG, "leaving disconnect"); + } + + @Override + public InputStream getInputStream() { + return inputStream; + } + + @Override + public OutputStream getOutputStream() { + return outputStream; + } + + @Override + public synchronized void waitForConnect() throws ConnectionLostException { + if (state != ConnectionState.INIT) { + throw new IllegalStateException("waitForConnect() may only be called once"); + } + if (open()) { + state = ConnectionState.CONNECTED; + } else { + state = ConnectionState.DISCONNECTED; + throw new ConnectionLostException(); + } + } + + @Override + protected void finalize() { + disconnect(); + } + + private boolean open() { + boolean result = false; + Log.i(TAG, "open() entered"); + + try { + FileDescriptor fd = fileDescriptor.getFileDescriptor(); + // If data is read from the InputStream created from this file descriptor all + // data of a USB transfer should be read at once. + inputStream = new FixedReadBufferedInputStream(new FileInputStream(fd), 1024); + outputStream = new BufferedOutputStream(new FileOutputStream(fd), 1024); + + // Soft-open the connection + outputStream.write(0x00); + outputStream.flush(); + + Log.i(TAG, "created streams"); + + // Ytai mentions http://code.google.com/p/android/issues/detail?id=20545 + // which is now closed, but waiting still seems to be required. + // "We're going to block now. We're counting on the IOIO to + // write back a byte, or otherwise we're locked until + // physical disconnection" + while (inputStream.read() != 1) { + wait(1000); + } + + Log.i(TAG, "success"); + result = true; + } catch (Exception e) { + Log.v(TAG, "Failed to open streams", e); + } finally { + if (!result) { + disconnect(); + } + } + return result; + } +} diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/pc/SerialPortIOIOConnection.java b/ioio/ioio/src/main/java/ioio/smallbasic/pc/SerialPortIOIOConnection.java index baf6dcd..a98148c 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/pc/SerialPortIOIOConnection.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/pc/SerialPortIOIOConnection.java @@ -1,14 +1,15 @@ package ioio.smallbasic.pc; import com.fazecast.jSerialComm.SerialPort; -import ioio.lib.api.IOIOConnection; -import ioio.lib.api.exception.ConnectionLostException; -import ioio.lib.spi.Log; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import ioio.lib.api.IOIOConnection; +import ioio.lib.api.exception.ConnectionLostException; +import ioio.lib.spi.Log; + public class SerialPortIOIOConnection implements IOIOConnection { private static final String TAG = "SerialPortIOIOConnection"; private static final int READ_TIMEOUT_MILLIS = 20000; diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/pc/SerialPortIOIOConnectionBootstrap.java b/ioio/ioio/src/main/java/ioio/smallbasic/pc/SerialPortIOIOConnectionBootstrap.java index eaec883..19f4bef 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/pc/SerialPortIOIOConnectionBootstrap.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/pc/SerialPortIOIOConnectionBootstrap.java @@ -30,10 +30,6 @@ package ioio.smallbasic.pc; import com.fazecast.jSerialComm.SerialPort; -import ioio.lib.api.IOIOConnection; -import ioio.lib.spi.IOIOConnectionBootstrap; -import ioio.lib.spi.IOIOConnectionFactory; -import ioio.lib.spi.Log; import java.util.Arrays; import java.util.Collection; @@ -41,6 +37,11 @@ import java.util.LinkedList; import java.util.List; +import ioio.lib.api.IOIOConnection; +import ioio.lib.spi.IOIOConnectionBootstrap; +import ioio.lib.spi.IOIOConnectionFactory; +import ioio.lib.spi.Log; + public class SerialPortIOIOConnectionBootstrap implements IOIOConnectionBootstrap { private static final String TAG = "SerialPortIOIOConnectionBootstrap"; diff --git a/ioio/ioio/src/main/res/xml/device_filter.xml b/ioio/ioio/src/main/res/xml/device_filter.xml new file mode 100644 index 0000000..e5bad3f --- /dev/null +++ b/ioio/ioio/src/main/res/xml/device_filter.xml @@ -0,0 +1,4 @@ + + + + From 5dd8e1c846bfa7b158a4ed57c3b61c29193803a8 Mon Sep 17 00:00:00 2001 From: chrisws Date: Fri, 29 Mar 2024 21:32:35 +1030 Subject: [PATCH 028/131] IOIO: make permission handling at least workable. --- ioio/ioio/src/main/AndroidManifest.xml | 4 +- .../src/main/java/ioio/smallbasic/IOLock.java | 3 +- .../main/java/ioio/smallbasic/IOService.java | 10 ++ .../android/AccessoryPermissionCheck.java | 54 +++------- .../smallbasic/android/AndroidLogger.java | 7 +- .../android/BluetoothConnection.java | 39 +++++--- .../android/FixedReadBufferedInputStream.java | 99 +++++++++++++++++++ 7 files changed, 157 insertions(+), 59 deletions(-) create mode 100644 ioio/ioio/src/main/java/ioio/smallbasic/android/FixedReadBufferedInputStream.java diff --git a/ioio/ioio/src/main/AndroidManifest.xml b/ioio/ioio/src/main/AndroidManifest.xml index 68ac02c..793b961 100644 --- a/ioio/ioio/src/main/AndroidManifest.xml +++ b/ioio/ioio/src/main/AndroidManifest.xml @@ -3,9 +3,9 @@ - + - + diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/IOLock.java b/ioio/ioio/src/main/java/ioio/smallbasic/IOLock.java index 9d555aa..d5aac23 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/IOLock.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/IOLock.java @@ -11,7 +11,7 @@ public class IOLock { private final Object mutex = new Object(); - private static final int TIMEOUT_SECS = 5; + private static final int TIMEOUT_SECS = 10; private Consumer consumer; public void invoke(Consumer consumer) { @@ -90,6 +90,7 @@ private CountDownLatch beginLatch() { private void endLatch(CountDownLatch latch) { try { if (!latch.await(TIMEOUT_SECS, TimeUnit.SECONDS)) { + IOService.setHardReset(true); throw new IOIOException("Timeout waiting for device"); } } catch (InterruptedException e) { diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/IOService.java b/ioio/ioio/src/main/java/ioio/smallbasic/IOService.java index e09927f..762781f 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/IOService.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/IOService.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import ioio.lib.api.IOIO; import ioio.lib.api.exception.ConnectionLostException; @@ -13,6 +14,7 @@ public class IOService implements IOIOLooperProvider { private static final String TAG = "IOService"; private static final int MAX_PINS = 46; + private static final AtomicBoolean HARD_RESET = new AtomicBoolean(false); private static IOService instance = null; private final ConnectionController connectionController; @@ -27,6 +29,10 @@ private IOService() { usedPins = new Boolean[MAX_PINS + 1]; } + public static boolean getHardReset() { + return HARD_RESET.get(); + } + public static IOService getInstance() { if (instance == null) { instance = new IOService(); @@ -34,6 +40,10 @@ public static IOService getInstance() { return instance; } + public static void setHardReset(boolean hardReset) { + IOService.HARD_RESET.set(hardReset); + } + public void addTask(IOTask ioTask) throws IOException { registerPin(ioTask.getPin()); ioTasks.add(ioTask); diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionCheck.java b/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionCheck.java index e55a07d..b53967b 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionCheck.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionCheck.java @@ -12,27 +12,17 @@ import android.os.Handler; import android.os.Looper; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - import ioio.lib.spi.Log; import ioio.smallbasic.IOIOException; public class AccessoryPermissionCheck extends BroadcastReceiver { private static final String TAG = AccessoryPermissionCheck.class.getSimpleName(); - private static final String ACTION_USB_PERMISSION = "android.hardware.usb.action.USB_PERMISSION"; - private static final String PERMISSION_ERROR = "Not permitted to use usb accessory."; - private static final long TIMEOUT_SECS = 30; - private final CountDownLatch latch; - private final AtomicBoolean permitted; + private static final String ACTION_USB_PERMISSION = "ioio.smallbasic.android.USB_PERMISSION"; + private static final String PERMISSION_ERROR = "Not permitted"; @TargetApi(Build.VERSION_CODES.TIRAMISU) public AccessoryPermissionCheck() { Log.d(TAG, "AccessoryPermissionCheck entered"); - this.permitted = new AtomicBoolean(false); - this.latch = new CountDownLatch(1); - UsbManager usbManager = (UsbManager) IOIOLoader.getContext().getSystemService(Context.USB_SERVICE); UsbAccessory[] accessories = usbManager.getAccessoryList(); UsbAccessory accessory = (accessories == null ? null : accessories[0]); @@ -43,43 +33,27 @@ public AccessoryPermissionCheck() { new Handler(Looper.getMainLooper()).post(() -> { Context context = IOIOLoader.getContext(); IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION); - context.registerReceiver(this, filter, Context.RECEIVER_EXPORTED); + filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY - 1); + context.registerReceiver(this, filter, Context.RECEIVER_NOT_EXPORTED); int flags = PendingIntent.FLAG_IMMUTABLE; Intent intent = new Intent(ACTION_USB_PERMISSION); PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, flags); usbManager.requestPermission(accessory, pendingIntent); }); - Log.d(TAG, "waiting for permission"); - try { - if (!latch.await(TIMEOUT_SECS, TimeUnit.SECONDS)) { - permissionError(); - } else { - IOIOLoader.getContext().unregisterReceiver(this); - } - } - catch (InterruptedException e) { - permissionError(); - throw new IOIOException(PERMISSION_ERROR); - } - if (!permitted.get()) { - permissionError(); - } + // for some reason using a latch caused an ANR here + Log.d(TAG, "requesting permission"); + throw new IOIOException(PERMISSION_ERROR); } } @Override - public void onReceive(Context context, Intent intent) { - Log.d(TAG, intent.getAction()); - synchronized (this) { - if (ACTION_USB_PERMISSION.equals(intent.getAction())) { - permitted.set(intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)); - latch.countDown(); - } + public synchronized void onReceive(final Context context, Intent intent) { + Log.d(TAG, "onReceive entered"); + if (ACTION_USB_PERMISSION.equals(intent.getAction())) { + final BroadcastReceiver receiver = this; + new Handler(Looper.getMainLooper()).post(() -> { + context.unregisterReceiver(receiver); + }); } } - - private void permissionError() { - IOIOLoader.getContext().unregisterReceiver(this); - throw new IOIOException(PERMISSION_ERROR); - } } diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/android/AndroidLogger.java b/ioio/ioio/src/main/java/ioio/smallbasic/android/AndroidLogger.java index d99d2a2..64f93bf 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/android/AndroidLogger.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/android/AndroidLogger.java @@ -1,5 +1,7 @@ package ioio.smallbasic.android; +import android.annotation.TargetApi; +import android.os.Build; import android.util.Log; public class AndroidLogger implements ioio.lib.spi.Log.ILogger { @@ -7,8 +9,11 @@ public AndroidLogger() { super(); } + @TargetApi(Build.VERSION_CODES.O) @Override public void write(int level, String tag, String message) { - Log.println(level, tag, message); + long id = Thread.currentThread().getId(); + String text = tag + ": [#" + id + "] " + message; + Log.println(Log.ERROR, "smallbasic", text); } } diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/android/BluetoothConnection.java b/ioio/ioio/src/main/java/ioio/smallbasic/android/BluetoothConnection.java index c82c101..3290a0e 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/android/BluetoothConnection.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/android/BluetoothConnection.java @@ -41,14 +41,18 @@ import ioio.lib.api.IOIOConnection; import ioio.lib.api.exception.ConnectionLostException; -import ioio.lib.impl.FixedReadBufferedInputStream; import ioio.lib.spi.Log; import ioio.smallbasic.IOIOException; +import ioio.smallbasic.IOService; class BluetoothConnection implements IOIOConnection { private static final String TAG = BluetoothConnection.class.getSimpleName(); + private static final int HARD_RESET = 0x00; + private static final int SOFT_RESET = 0x01; + private static final int ESTABLISH_CONNECTION = 0x00; + private ConnectionState state; - private InputStream inputStream; + private FixedReadBufferedInputStream inputStream; private OutputStream outputStream; private ParcelFileDescriptor fileDescriptor; @@ -126,22 +130,27 @@ private boolean open() { inputStream = new FixedReadBufferedInputStream(new FileInputStream(fd), 1024); outputStream = new BufferedOutputStream(new FileOutputStream(fd), 1024); - // Soft-open the connection - outputStream.write(0x00); + // open the connection + if (IOService.getHardReset()) { + outputStream.write(HARD_RESET); + outputStream.write('I'); + outputStream.write('O'); + outputStream.write('I'); + outputStream.write('O'); + IOService.setHardReset(false); + } else { + outputStream.write(SOFT_RESET); + } outputStream.flush(); - Log.i(TAG, "created streams"); - - // Ytai mentions http://code.google.com/p/android/issues/detail?id=20545 - // which is now closed, but waiting still seems to be required. - // "We're going to block now. We're counting on the IOIO to - // write back a byte, or otherwise we're locked until - // physical disconnection" - while (inputStream.read() != 1) { - wait(1000); + // ensure IOIOProtocol.run() first see's ESTABLISH_CONNECTION(0x00) and not SOFT_RESET(0x01) + // to avoid an NPE in IncomingState.handleSoftReset(). This method relies on initialisation in + // IncomingState.handleEstablishConnection + if (inputStream.positionTo(ESTABLISH_CONNECTION)) { + Log.i(TAG, "created streams"); + } else { + Log.i(TAG, "created streams - failed to position data"); } - - Log.i(TAG, "success"); result = true; } catch (Exception e) { Log.v(TAG, "Failed to open streams", e); diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/android/FixedReadBufferedInputStream.java b/ioio/ioio/src/main/java/ioio/smallbasic/android/FixedReadBufferedInputStream.java new file mode 100644 index 0000000..ffbae16 --- /dev/null +++ b/ioio/ioio/src/main/java/ioio/smallbasic/android/FixedReadBufferedInputStream.java @@ -0,0 +1,99 @@ +package ioio.smallbasic.android; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Similar to a {@link BufferedInputStream}, but guarantees that all reads from the source stream + * are exactly the specified size of the buffer. + * It turns out, {@link BufferedInputStream} does not actually have such a guarantee. + */ +public class FixedReadBufferedInputStream extends InputStream { + private int bufferIndex_ = 0; + private int validData_ = 0; + private final byte[] buffer_; + private final InputStream source_; + + public FixedReadBufferedInputStream(InputStream source, int size) { + buffer_ = new byte[size]; + source_ = source; + } + + @Override + public int available() throws IOException { + return validData_ - bufferIndex_; + } + + @Override + public void close() throws IOException { + source_.close(); + } + + @Override + public int read(byte[] buffer, int offset, int length) throws IOException { + fillIfEmpty(); + if (validData_ == -1) { + return -1; + } + length = Math.min(length, validData_ - bufferIndex_); + System.arraycopy(buffer_, bufferIndex_, buffer, offset, length); + bufferIndex_ += length; + return length; + } + + @Override + public int read(byte[] buffer) throws IOException { + return read(buffer, 0, buffer.length); + } + + @Override + public int read() throws IOException { + fillIfEmpty(); + if (validData_ == -1) { + return -1; + } + return buffer_[bufferIndex_++] & 0xFF; + } + + @Override + public long skip(long byteCount) throws IOException { + long skipped = 0; + while (byteCount > 0) { + fillIfEmpty(); + if (validData_ == -1) { + return skipped; + } + int count = (int) Math.min(available(), byteCount); + byteCount -= count; + bufferIndex_ += count; + skipped += count; + } + return skipped; + } + + protected boolean positionTo(int value) throws IOException { + fillIfEmpty(); + if (validData_ != -1) { + while (bufferIndex_ < buffer_.length) { + if (buffer_[bufferIndex_] == value) { + break; + } else { + bufferIndex_++; + } + } + } + return validData_ != -1; + } + + private void fill() throws IOException { + bufferIndex_ = 0; + validData_ = source_.read(buffer_); + } + + private void fillIfEmpty() throws IOException { + while (available() == 0 && validData_ != -1) { + fill(); + } + } +} From 4a370796dbf11ed61510bead35be3e08d66514ff Mon Sep 17 00:00:00 2001 From: chrisws Date: Sat, 30 Mar 2024 20:42:31 +1030 Subject: [PATCH 029/131] IOIO: rework usb handling --- .../java/ioio/smallbasic/IOIOException.java | 13 +-- .../main/java/ioio/smallbasic/IOIOImpl.java | 1 + .../src/main/java/ioio/smallbasic/IOLock.java | 1 - .../main/java/ioio/smallbasic/IOService.java | 4 +- .../src/main/java/ioio/smallbasic/IOTask.java | 11 +-- .../src/main/java/ioio/smallbasic/IOUtil.java | 23 +++++ .../java/ioio/smallbasic/SpiMasterImpl.java | 4 +- .../android/AccessoryConnectionBootstrap.java | 25 +---- .../android/AccessoryPermissionCheck.java | 8 +- .../android/FixedReadBufferedInputStream.java | 99 ------------------- ...oothConnection.java => UsbConnection.java} | 86 +++++++--------- .../java/ioio/smallbasic/android/UsbUtil.java | 27 +++++ .../ioio/smallbasic/DigitalOutputTest.java | 2 - 13 files changed, 109 insertions(+), 195 deletions(-) create mode 100644 ioio/ioio/src/main/java/ioio/smallbasic/IOUtil.java delete mode 100644 ioio/ioio/src/main/java/ioio/smallbasic/android/FixedReadBufferedInputStream.java rename ioio/ioio/src/main/java/ioio/smallbasic/android/{BluetoothConnection.java => UsbConnection.java} (64%) create mode 100644 ioio/ioio/src/main/java/ioio/smallbasic/android/UsbUtil.java diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/IOIOException.java b/ioio/ioio/src/main/java/ioio/smallbasic/IOIOException.java index 9ef7a8d..33a253d 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/IOIOException.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/IOIOException.java @@ -1,19 +1,20 @@ package ioio.smallbasic; public class IOIOException extends RuntimeException { + public IOIOException() { + super(); + } + public IOIOException(Exception exception) { - super(exception.getMessage()); + this(exception.getMessage()); } public IOIOException(String message) { super(message); + IOUtil.setError(message); } public String getMessage() { - String result = super.getMessage(); - if (result != null) { - result = result.replace("ioio.smallbasic.IOIOException:", "").trim(); - } - return result; + return IOUtil.getError(); } } diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/IOIOImpl.java b/ioio/ioio/src/main/java/ioio/smallbasic/IOIOImpl.java index b633cee..fab6f04 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/IOIOImpl.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/IOIOImpl.java @@ -60,6 +60,7 @@ public void sync() { } public void waitForConnect(int latency) { + IOUtil.setError(null); TimerUtil.setLatency(latency); IOService.getInstance().start(); lock.invoke(IOIO::waitForConnect); diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/IOLock.java b/ioio/ioio/src/main/java/ioio/smallbasic/IOLock.java index d5aac23..6ded9bf 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/IOLock.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/IOLock.java @@ -90,7 +90,6 @@ private CountDownLatch beginLatch() { private void endLatch(CountDownLatch latch) { try { if (!latch.await(TIMEOUT_SECS, TimeUnit.SECONDS)) { - IOService.setHardReset(true); throw new IOIOException("Timeout waiting for device"); } } catch (InterruptedException e) { diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/IOService.java b/ioio/ioio/src/main/java/ioio/smallbasic/IOService.java index 762781f..8d3e239 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/IOService.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/IOService.java @@ -111,7 +111,7 @@ public void loop() throws ConnectionLostException, InterruptedException { try { next.loop(); } catch (Throwable e) { - next.setError(e.getLocalizedMessage()); + IOUtil.setError(e.getLocalizedMessage()); break; } } @@ -125,7 +125,7 @@ public void setup(IOIO ioio) { try { next.setup(ioio); } catch (Throwable e) { - next.setError(e.getLocalizedMessage()); + IOUtil.setError(e.getLocalizedMessage()); break; } } diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/IOTask.java b/ioio/ioio/src/main/java/ioio/smallbasic/IOTask.java index ddb5c2c..f04fb5f 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/IOTask.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/IOTask.java @@ -2,7 +2,6 @@ import java.io.Closeable; import java.io.IOException; -import java.util.concurrent.atomic.AtomicReference; import ioio.lib.api.IOIO; import ioio.lib.api.exception.ConnectionLostException; @@ -14,7 +13,6 @@ */ public abstract class IOTask implements Closeable { protected int pin; - private AtomicReference error; @Override public void close() { @@ -22,21 +20,16 @@ public void close() { } public void handleError() { - if (error.get() != null) { - throw new IOIOException(error.get()); + if (IOUtil.getError() != null) { + throw new IOIOException(); } } public void open(int pin) throws IOException { this.pin = pin; - this.error = new AtomicReference<>(null); IOService.getInstance().addTask(this); } - public void setError(String error) { - this.error.set(error); - } - int getPin() { return pin; } diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/IOUtil.java b/ioio/ioio/src/main/java/ioio/smallbasic/IOUtil.java new file mode 100644 index 0000000..2d34e1f --- /dev/null +++ b/ioio/ioio/src/main/java/ioio/smallbasic/IOUtil.java @@ -0,0 +1,23 @@ +package ioio.smallbasic; + +import java.util.concurrent.atomic.AtomicReference; + +public class IOUtil { + private static final AtomicReference ERROR = new AtomicReference<>(); + + private IOUtil() { + // no access + } + + public static String getError() { + return ERROR.get(); + } + + public static synchronized void setError(String error) { + if (error != null && ERROR.get() != null && !ERROR.get().contains(error)) { + ERROR.set(error + " [" + ERROR.get() + "]"); + } else { + ERROR.set(error); + } + } +} diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/SpiMasterImpl.java b/ioio/ioio/src/main/java/ioio/smallbasic/SpiMasterImpl.java index ec74e05..cb87463 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/SpiMasterImpl.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/SpiMasterImpl.java @@ -59,7 +59,7 @@ void setup(IOIO ioio) throws ConnectionLostException { } private void pinError(String name) { - setError("Incorrect " + name + " pin value"); + IOUtil.setError("Incorrect " + name + " pin value"); } private void validatePins() { @@ -77,7 +77,7 @@ private void validatePins() { mosi == clk || mosi == slaveSelect || clk == slaveSelect) { - setError("One or pins have duplicate values"); + IOUtil.setError("One or pins have duplicate values"); } } } diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryConnectionBootstrap.java b/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryConnectionBootstrap.java index 23c11c7..1364c44 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryConnectionBootstrap.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryConnectionBootstrap.java @@ -29,32 +29,20 @@ package ioio.smallbasic.android; -import android.content.Context; -import android.hardware.usb.UsbAccessory; -import android.hardware.usb.UsbManager; - import java.util.Collection; import ioio.lib.api.IOIOConnection; import ioio.lib.spi.IOIOConnectionBootstrap; import ioio.lib.spi.IOIOConnectionFactory; -import ioio.lib.spi.Log; -import ioio.lib.spi.NoRuntimeSupportException; public class AccessoryConnectionBootstrap implements IOIOConnectionBootstrap, IOIOConnectionFactory { - private static final String TAG = AccessoryConnectionBootstrap.class.getSimpleName(); - - public AccessoryConnectionBootstrap() throws NoRuntimeSupportException { - Log.d(TAG, "creating AccessoryConnectionBootstrap"); + public AccessoryConnectionBootstrap() { + super(); } @Override public IOIOConnection createConnection() { - Log.i(TAG, "createConnection"); - Context activity = IOIOLoader.getContext(); - UsbManager usbManager = (UsbManager) activity.getSystemService(Context.USB_SERVICE); - UsbAccessory accessory = usbManager.getAccessoryList()[0]; - return new BluetoothConnection(getUsbManager().openAccessory(accessory)); + return new UsbConnection(); } @Override @@ -69,11 +57,6 @@ public void getFactories(Collection result) { @Override public String getType() { - return BluetoothConnection.class.getCanonicalName(); - } - - private UsbManager getUsbManager() { - Context activity = IOIOLoader.getContext(); - return (UsbManager) activity.getSystemService(Context.USB_SERVICE); + return UsbConnection.class.getCanonicalName(); } } diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionCheck.java b/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionCheck.java index b53967b..46533c5 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionCheck.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionCheck.java @@ -23,12 +23,12 @@ public class AccessoryPermissionCheck extends BroadcastReceiver { @TargetApi(Build.VERSION_CODES.TIRAMISU) public AccessoryPermissionCheck() { Log.d(TAG, "AccessoryPermissionCheck entered"); - UsbManager usbManager = (UsbManager) IOIOLoader.getContext().getSystemService(Context.USB_SERVICE); - UsbAccessory[] accessories = usbManager.getAccessoryList(); - UsbAccessory accessory = (accessories == null ? null : accessories[0]); + UsbAccessory accessory = UsbUtil.getUsbAccessory(); if (accessory == null) { throw new IOIOException("No usb accessory found."); } + + UsbManager usbManager = (UsbManager) IOIOLoader.getContext().getSystemService(Context.USB_SERVICE); if (!usbManager.hasPermission(accessory)) { new Handler(Looper.getMainLooper()).post(() -> { Context context = IOIOLoader.getContext(); @@ -40,7 +40,7 @@ public AccessoryPermissionCheck() { PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, flags); usbManager.requestPermission(accessory, pendingIntent); }); - // for some reason using a latch caused an ANR here + // for some reason using a latch here caused an ANR Log.d(TAG, "requesting permission"); throw new IOIOException(PERMISSION_ERROR); } diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/android/FixedReadBufferedInputStream.java b/ioio/ioio/src/main/java/ioio/smallbasic/android/FixedReadBufferedInputStream.java deleted file mode 100644 index ffbae16..0000000 --- a/ioio/ioio/src/main/java/ioio/smallbasic/android/FixedReadBufferedInputStream.java +++ /dev/null @@ -1,99 +0,0 @@ -package ioio.smallbasic.android; - -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStream; - -/** - * Similar to a {@link BufferedInputStream}, but guarantees that all reads from the source stream - * are exactly the specified size of the buffer. - * It turns out, {@link BufferedInputStream} does not actually have such a guarantee. - */ -public class FixedReadBufferedInputStream extends InputStream { - private int bufferIndex_ = 0; - private int validData_ = 0; - private final byte[] buffer_; - private final InputStream source_; - - public FixedReadBufferedInputStream(InputStream source, int size) { - buffer_ = new byte[size]; - source_ = source; - } - - @Override - public int available() throws IOException { - return validData_ - bufferIndex_; - } - - @Override - public void close() throws IOException { - source_.close(); - } - - @Override - public int read(byte[] buffer, int offset, int length) throws IOException { - fillIfEmpty(); - if (validData_ == -1) { - return -1; - } - length = Math.min(length, validData_ - bufferIndex_); - System.arraycopy(buffer_, bufferIndex_, buffer, offset, length); - bufferIndex_ += length; - return length; - } - - @Override - public int read(byte[] buffer) throws IOException { - return read(buffer, 0, buffer.length); - } - - @Override - public int read() throws IOException { - fillIfEmpty(); - if (validData_ == -1) { - return -1; - } - return buffer_[bufferIndex_++] & 0xFF; - } - - @Override - public long skip(long byteCount) throws IOException { - long skipped = 0; - while (byteCount > 0) { - fillIfEmpty(); - if (validData_ == -1) { - return skipped; - } - int count = (int) Math.min(available(), byteCount); - byteCount -= count; - bufferIndex_ += count; - skipped += count; - } - return skipped; - } - - protected boolean positionTo(int value) throws IOException { - fillIfEmpty(); - if (validData_ != -1) { - while (bufferIndex_ < buffer_.length) { - if (buffer_[bufferIndex_] == value) { - break; - } else { - bufferIndex_++; - } - } - } - return validData_ != -1; - } - - private void fill() throws IOException { - bufferIndex_ = 0; - validData_ = source_.read(buffer_); - } - - private void fillIfEmpty() throws IOException { - while (available() == 0 && validData_ != -1) { - fill(); - } - } -} diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/android/BluetoothConnection.java b/ioio/ioio/src/main/java/ioio/smallbasic/android/UsbConnection.java similarity index 64% rename from ioio/ioio/src/main/java/ioio/smallbasic/android/BluetoothConnection.java rename to ioio/ioio/src/main/java/ioio/smallbasic/android/UsbConnection.java index 3290a0e..fe20dda 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/android/BluetoothConnection.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/android/UsbConnection.java @@ -35,22 +35,18 @@ import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileOutputStream; -import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import ioio.lib.api.IOIOConnection; -import ioio.lib.api.exception.ConnectionLostException; +import ioio.lib.impl.FixedReadBufferedInputStream; import ioio.lib.spi.Log; import ioio.smallbasic.IOIOException; -import ioio.smallbasic.IOService; +import ioio.smallbasic.IOUtil; -class BluetoothConnection implements IOIOConnection { - private static final String TAG = BluetoothConnection.class.getSimpleName(); - private static final int HARD_RESET = 0x00; +class UsbConnection implements IOIOConnection { + private static final String TAG = UsbConnection.class.getSimpleName(); private static final int SOFT_RESET = 0x01; - private static final int ESTABLISH_CONNECTION = 0x00; - private ConnectionState state; private FixedReadBufferedInputStream inputStream; private OutputStream outputStream; @@ -60,15 +56,12 @@ private enum ConnectionState { INIT, CONNECTED, DISCONNECTED } - BluetoothConnection(ParcelFileDescriptor fileDescriptor) { - Log.i(TAG, "creating BluetoothConnection"); - this.fileDescriptor = fileDescriptor; + UsbConnection() { + Log.i(TAG, "creating UsbConnection"); this.state = ConnectionState.INIT; this.inputStream = null; this.outputStream = null; - if (this.fileDescriptor == null) { - throw new IOIOException("Failed to obtain descriptor"); - } + this.fileDescriptor = null; } @Override @@ -79,14 +72,18 @@ public boolean canClose() { @Override public synchronized void disconnect() { Log.i(TAG, "disconnect entered"); - this.state = ConnectionState.DISCONNECTED; - if (fileDescriptor != null) { - try { - fileDescriptor.close(); - } catch (IOException e) { - Log.e(TAG, "Failed to close file descriptor.", e); + if (this.state != ConnectionState.DISCONNECTED) { + this.state = ConnectionState.DISCONNECTED; + IOUtil.setError("USB disconnected"); + if (fileDescriptor != null) { + try { + fileDescriptor.close(); + } catch (java.io.IOException e) { + IOUtil.setError("Failed to close file descriptor"); + Log.e(TAG, "Failed to close file descriptor.", e); + } + fileDescriptor = null; } - fileDescriptor = null; } Log.d(TAG, "leaving disconnect"); } @@ -102,15 +99,18 @@ public OutputStream getOutputStream() { } @Override - public synchronized void waitForConnect() throws ConnectionLostException { + public synchronized void waitForConnect() { if (state != ConnectionState.INIT) { throw new IllegalStateException("waitForConnect() may only be called once"); } + this.fileDescriptor = UsbUtil.getParcelFileDescriptor(); + if (this.fileDescriptor == null) { + throw new IOIOException("Failed to obtain descriptor"); + } if (open()) { state = ConnectionState.CONNECTED; } else { - state = ConnectionState.DISCONNECTED; - throw new ConnectionLostException(); + throw new IOIOException("USB connection lost"); } } @@ -125,38 +125,26 @@ private boolean open() { try { FileDescriptor fd = fileDescriptor.getFileDescriptor(); - // If data is read from the InputStream created from this file descriptor all - // data of a USB transfer should be read at once. inputStream = new FixedReadBufferedInputStream(new FileInputStream(fd), 1024); outputStream = new BufferedOutputStream(new FileOutputStream(fd), 1024); - - // open the connection - if (IOService.getHardReset()) { - outputStream.write(HARD_RESET); - outputStream.write('I'); - outputStream.write('O'); - outputStream.write('I'); - outputStream.write('O'); - IOService.setHardReset(false); - } else { - outputStream.write(SOFT_RESET); - } + outputStream.write(SOFT_RESET); outputStream.flush(); - - // ensure IOIOProtocol.run() first see's ESTABLISH_CONNECTION(0x00) and not SOFT_RESET(0x01) - // to avoid an NPE in IncomingState.handleSoftReset(). This method relies on initialisation in - // IncomingState.handleEstablishConnection - if (inputStream.positionTo(ESTABLISH_CONNECTION)) { - Log.i(TAG, "created streams"); + int response = inputStream.read(); + if (response == SOFT_RESET) { + result = true; } else { - Log.i(TAG, "created streams - failed to position data"); + IOUtil.setError("Unexpected response: " + response); } - result = true; - } catch (Exception e) { - Log.v(TAG, "Failed to open streams", e); + } catch (java.io.IOException e) { + IOUtil.setError("Failed to open streams: " + e); } finally { if (!result) { - disconnect(); + try { + fileDescriptor.close(); + } catch (java.io.IOException e) { + Log.e(TAG, "Failed to close file descriptor.", e); + } + fileDescriptor = null; } } return result; diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/android/UsbUtil.java b/ioio/ioio/src/main/java/ioio/smallbasic/android/UsbUtil.java new file mode 100644 index 0000000..ed5161e --- /dev/null +++ b/ioio/ioio/src/main/java/ioio/smallbasic/android/UsbUtil.java @@ -0,0 +1,27 @@ +package ioio.smallbasic.android; + +import android.content.Context; +import android.hardware.usb.UsbAccessory; +import android.hardware.usb.UsbManager; +import android.os.ParcelFileDescriptor; + +public class UsbUtil { + private UsbUtil() { + // no access + } + + static ParcelFileDescriptor getParcelFileDescriptor() { + return getUsbManager().openAccessory(getUsbAccessory()); + } + + static UsbAccessory getUsbAccessory() { + UsbManager usbManager = (UsbManager) IOIOLoader.getContext().getSystemService(Context.USB_SERVICE); + UsbAccessory[] accessories = usbManager.getAccessoryList(); + return (accessories == null ? null : accessories[0]); + } + + static UsbManager getUsbManager() { + Context activity = IOIOLoader.getContext(); + return (UsbManager) activity.getSystemService(Context.USB_SERVICE); + } +} diff --git a/ioio/ioio/src/test/java/ioio/smallbasic/DigitalOutputTest.java b/ioio/ioio/src/test/java/ioio/smallbasic/DigitalOutputTest.java index fd956e9..a72b613 100644 --- a/ioio/ioio/src/test/java/ioio/smallbasic/DigitalOutputTest.java +++ b/ioio/ioio/src/test/java/ioio/smallbasic/DigitalOutputTest.java @@ -4,8 +4,6 @@ import ioio.lib.api.IOIO; import ioio.lib.api.exception.ConnectionLostException; -import ioio.smallbasic.DigitalOutputImpl; -import ioio.smallbasic.IOService; class DigitalOutputTest { public static void main(String[] args) throws InterruptedException, ConnectionLostException, IOException { From e42ec3d21ffa967b6f0074a2c4f8234e4f9ee506 Mon Sep 17 00:00:00 2001 From: chrisws Date: Sun, 31 Mar 2024 11:22:00 +1030 Subject: [PATCH 030/131] IOIO: make the android usb connection more reliable --- .../main/java/ioio/smallbasic/IOIOImpl.java | 9 +- .../src/main/java/ioio/smallbasic/IOUtil.java | 10 ++ .../android/AccessoryPermissionCheck.java | 10 +- .../smallbasic/android/UsbConnection.java | 105 ++++++++++++------ 4 files changed, 100 insertions(+), 34 deletions(-) diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/IOIOImpl.java b/ioio/ioio/src/main/java/ioio/smallbasic/IOIOImpl.java index fab6f04..7ed99a0 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/IOIOImpl.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/IOIOImpl.java @@ -61,9 +61,16 @@ public void sync() { public void waitForConnect(int latency) { IOUtil.setError(null); - TimerUtil.setLatency(latency); + if (latency < 0) { + IOUtil.setHardReset(true); + } else { + IOUtil.setHardReset(false); + TimerUtil.setLatency(latency); + } IOService.getInstance().start(); + handleError(); lock.invoke(IOIO::waitForConnect); + handleError(); } public void waitForDisconnect() { diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/IOUtil.java b/ioio/ioio/src/main/java/ioio/smallbasic/IOUtil.java index 2d34e1f..ce3f4c3 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/IOUtil.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/IOUtil.java @@ -1,9 +1,11 @@ package ioio.smallbasic; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; public class IOUtil { private static final AtomicReference ERROR = new AtomicReference<>(); + private static final AtomicBoolean HARD_RESET = new AtomicBoolean(false); private IOUtil() { // no access @@ -13,6 +15,10 @@ public static String getError() { return ERROR.get(); } + public static boolean getHardReset() { + return HARD_RESET.get(); + } + public static synchronized void setError(String error) { if (error != null && ERROR.get() != null && !ERROR.get().contains(error)) { ERROR.set(error + " [" + ERROR.get() + "]"); @@ -20,4 +26,8 @@ public static synchronized void setError(String error) { ERROR.set(error); } } + + public static void setHardReset(boolean hardReset) { + HARD_RESET.set(hardReset); + } } diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionCheck.java b/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionCheck.java index 46533c5..9be6ce2 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionCheck.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionCheck.java @@ -11,9 +11,11 @@ import android.os.Build; import android.os.Handler; import android.os.Looper; +import android.widget.Toast; import ioio.lib.spi.Log; import ioio.smallbasic.IOIOException; +import ioio.smallbasic.IOUtil; public class AccessoryPermissionCheck extends BroadcastReceiver { private static final String TAG = AccessoryPermissionCheck.class.getSimpleName(); @@ -28,7 +30,7 @@ public AccessoryPermissionCheck() { throw new IOIOException("No usb accessory found."); } - UsbManager usbManager = (UsbManager) IOIOLoader.getContext().getSystemService(Context.USB_SERVICE); + UsbManager usbManager = UsbUtil.getUsbManager(); if (!usbManager.hasPermission(accessory)) { new Handler(Looper.getMainLooper()).post(() -> { Context context = IOIOLoader.getContext(); @@ -40,8 +42,9 @@ public AccessoryPermissionCheck() { PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, flags); usbManager.requestPermission(accessory, pendingIntent); }); - // for some reason using a latch here caused an ANR + // for some reason using a latch here causes an ANR Log.d(TAG, "requesting permission"); + IOUtil.setHardReset(true); throw new IOIOException(PERMISSION_ERROR); } } @@ -50,8 +53,11 @@ public AccessoryPermissionCheck() { public synchronized void onReceive(final Context context, Intent intent) { Log.d(TAG, "onReceive entered"); if (ACTION_USB_PERMISSION.equals(intent.getAction())) { + boolean permitted = UsbUtil.getUsbManager().hasPermission(UsbUtil.getUsbAccessory()); + final String message = "USB access " + (permitted ? "permitted" : "denied"); final BroadcastReceiver receiver = this; new Handler(Looper.getMainLooper()).post(() -> { + Toast.makeText(context, message, Toast.LENGTH_LONG).show(); context.unregisterReceiver(receiver); }); } diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/android/UsbConnection.java b/ioio/ioio/src/main/java/ioio/smallbasic/android/UsbConnection.java index fe20dda..3ed1671 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/android/UsbConnection.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/android/UsbConnection.java @@ -35,6 +35,7 @@ import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileOutputStream; +import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -47,6 +48,8 @@ class UsbConnection implements IOIOConnection { private static final String TAG = UsbConnection.class.getSimpleName(); private static final int SOFT_RESET = 0x01; + private static final int HARD_RESET = 0x00; + private static final int MAX_RETRIES = 10; private ConnectionState state; private FixedReadBufferedInputStream inputStream; private OutputStream outputStream; @@ -73,17 +76,8 @@ public boolean canClose() { public synchronized void disconnect() { Log.i(TAG, "disconnect entered"); if (this.state != ConnectionState.DISCONNECTED) { - this.state = ConnectionState.DISCONNECTED; IOUtil.setError("USB disconnected"); - if (fileDescriptor != null) { - try { - fileDescriptor.close(); - } catch (java.io.IOException e) { - IOUtil.setError("Failed to close file descriptor"); - Log.e(TAG, "Failed to close file descriptor.", e); - } - fileDescriptor = null; - } + close(); } Log.d(TAG, "leaving disconnect"); } @@ -103,10 +97,6 @@ public synchronized void waitForConnect() { if (state != ConnectionState.INIT) { throw new IllegalStateException("waitForConnect() may only be called once"); } - this.fileDescriptor = UsbUtil.getParcelFileDescriptor(); - if (this.fileDescriptor == null) { - throw new IOIOException("Failed to obtain descriptor"); - } if (open()) { state = ConnectionState.CONNECTED; } else { @@ -119,34 +109,87 @@ protected void finalize() { disconnect(); } + private void close() { + state = ConnectionState.DISCONNECTED; + try { + if (inputStream != null) { + inputStream.close(); + } + if (outputStream != null) { + outputStream.close(); + } + if (fileDescriptor != null) { + fileDescriptor.close(); + } + inputStream = null; + outputStream = null; + fileDescriptor = null; + } catch (java.io.IOException e) { + IOUtil.setError("Failed to close file descriptor: " + e); + Log.e(TAG, "Failed to close file descriptor.", e); + } + } + + private void handleResetResponse(int attempt) throws IOException { + if (attempt > 0) { + int response = inputStream.read(); + Log.e(TAG, "Response:" + response + " available:" + inputStream.available()); + if (response != SOFT_RESET) { + // fail + if (inputStream.available() == 0) { + try { + Thread.sleep(100); + } + catch (InterruptedException e) { + throw new IOIOException(e); + } + } + handleResetResponse(attempt - 1); + } + } else { + throw new IOIOException("USB connection failure"); + } + } + private boolean open() { boolean result = false; Log.i(TAG, "open() entered"); try { - FileDescriptor fd = fileDescriptor.getFileDescriptor(); - inputStream = new FixedReadBufferedInputStream(new FileInputStream(fd), 1024); - outputStream = new BufferedOutputStream(new FileOutputStream(fd), 1024); - outputStream.write(SOFT_RESET); - outputStream.flush(); - int response = inputStream.read(); - if (response == SOFT_RESET) { - result = true; - } else { - IOUtil.setError("Unexpected response: " + response); - } + openStreams(); + resetBoard(); + handleResetResponse(MAX_RETRIES); + result = true; } catch (java.io.IOException e) { IOUtil.setError("Failed to open streams: " + e); } finally { if (!result) { - try { - fileDescriptor.close(); - } catch (java.io.IOException e) { - Log.e(TAG, "Failed to close file descriptor.", e); - } - fileDescriptor = null; + close(); } } return result; } + + private void openStreams() { + this.fileDescriptor = UsbUtil.getParcelFileDescriptor(); + if (this.fileDescriptor == null) { + throw new IOIOException("Failed to obtain descriptor"); + } + FileDescriptor fd = fileDescriptor.getFileDescriptor(); + inputStream = new FixedReadBufferedInputStream(new FileInputStream(fd), 1024); + outputStream = new BufferedOutputStream(new FileOutputStream(fd), 1024); + } + + private void resetBoard() throws IOException { + if (IOUtil.getHardReset()) { + outputStream.write(HARD_RESET); + outputStream.write('I'); + outputStream.write('O'); + outputStream.write('I'); + outputStream.write('O'); + } else { + outputStream.write(SOFT_RESET); + } + outputStream.flush(); + } } From 8079a7642e0a3fc17d29a4a166295616bae127a4 Mon Sep 17 00:00:00 2001 From: chrisws Date: Sun, 31 Mar 2024 16:09:18 +1030 Subject: [PATCH 031/131] IOIO: sync instead of soft reset seems to have better success --- .../main/java/ioio/smallbasic/IOIOImpl.java | 2 +- .../android/AccessoryPermissionCheck.java | 1 - .../smallbasic/android/UsbConnection.java | 26 +++++++++++-------- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/IOIOImpl.java b/ioio/ioio/src/main/java/ioio/smallbasic/IOIOImpl.java index 7ed99a0..d09c0db 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/IOIOImpl.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/IOIOImpl.java @@ -64,13 +64,13 @@ public void waitForConnect(int latency) { if (latency < 0) { IOUtil.setHardReset(true); } else { - IOUtil.setHardReset(false); TimerUtil.setLatency(latency); } IOService.getInstance().start(); handleError(); lock.invoke(IOIO::waitForConnect); handleError(); + IOUtil.setHardReset(false); } public void waitForDisconnect() { diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionCheck.java b/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionCheck.java index 9be6ce2..0bf4104 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionCheck.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionCheck.java @@ -44,7 +44,6 @@ public AccessoryPermissionCheck() { }); // for some reason using a latch here causes an ANR Log.d(TAG, "requesting permission"); - IOUtil.setHardReset(true); throw new IOIOException(PERMISSION_ERROR); } } diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/android/UsbConnection.java b/ioio/ioio/src/main/java/ioio/smallbasic/android/UsbConnection.java index 3ed1671..53db954 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/android/UsbConnection.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/android/UsbConnection.java @@ -49,6 +49,7 @@ class UsbConnection implements IOIOConnection { private static final String TAG = UsbConnection.class.getSimpleName(); private static final int SOFT_RESET = 0x01; private static final int HARD_RESET = 0x00; + private static final int SYNC = 0x23; private static final int MAX_RETRIES = 10; private ConnectionState state; private FixedReadBufferedInputStream inputStream; @@ -60,7 +61,7 @@ private enum ConnectionState { } UsbConnection() { - Log.i(TAG, "creating UsbConnection"); + Log.d(TAG, "creating UsbConnection"); this.state = ConnectionState.INIT; this.inputStream = null; this.outputStream = null; @@ -74,8 +75,8 @@ public boolean canClose() { @Override public synchronized void disconnect() { - Log.i(TAG, "disconnect entered"); - if (this.state != ConnectionState.DISCONNECTED) { + Log.d(TAG, "disconnect entered: " + state); + if (state != ConnectionState.DISCONNECTED) { IOUtil.setError("USB disconnected"); close(); } @@ -110,6 +111,7 @@ protected void finalize() { } private void close() { + Log.d(TAG, "close streams"); state = ConnectionState.DISCONNECTED; try { if (inputStream != null) { @@ -131,12 +133,12 @@ private void close() { } private void handleResetResponse(int attempt) throws IOException { - if (attempt > 0) { + if (attempt < MAX_RETRIES) { int response = inputStream.read(); - Log.e(TAG, "Response:" + response + " available:" + inputStream.available()); + Log.d(TAG, "Response:" + response + " available:" + inputStream.available() + " attempt:" + attempt); if (response != SOFT_RESET) { - // fail - if (inputStream.available() == 0) { + // unexpected + if (inputStream.available() < 1) { try { Thread.sleep(100); } @@ -144,7 +146,7 @@ private void handleResetResponse(int attempt) throws IOException { throw new IOIOException(e); } } - handleResetResponse(attempt - 1); + handleResetResponse(attempt + 1); } } else { throw new IOIOException("USB connection failure"); @@ -153,12 +155,12 @@ private void handleResetResponse(int attempt) throws IOException { private boolean open() { boolean result = false; - Log.i(TAG, "open() entered"); + Log.d(TAG, "open() entered"); try { openStreams(); resetBoard(); - handleResetResponse(MAX_RETRIES); + handleResetResponse(0); result = true; } catch (java.io.IOException e) { IOUtil.setError("Failed to open streams: " + e); @@ -182,13 +184,15 @@ private void openStreams() { private void resetBoard() throws IOException { if (IOUtil.getHardReset()) { + Log.d(TAG, "hard reset"); outputStream.write(HARD_RESET); outputStream.write('I'); outputStream.write('O'); outputStream.write('I'); outputStream.write('O'); } else { - outputStream.write(SOFT_RESET); + Log.d(TAG, "soft reset"); + outputStream.write(SYNC); } outputStream.flush(); } From 036e8c16c43315ce2b723dedfe5401395f023aba Mon Sep 17 00:00:00 2001 From: chrisws Date: Mon, 1 Apr 2024 09:51:15 +1030 Subject: [PATCH 032/131] IOIO: update permission error text --- .../smallbasic/android/AccessoryPermissionCheck.java | 11 ++++++----- .../java/ioio/smallbasic/android/UsbConnection.java | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionCheck.java b/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionCheck.java index 0bf4104..41f65db 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionCheck.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionCheck.java @@ -15,7 +15,6 @@ import ioio.lib.spi.Log; import ioio.smallbasic.IOIOException; -import ioio.smallbasic.IOUtil; public class AccessoryPermissionCheck extends BroadcastReceiver { private static final String TAG = AccessoryPermissionCheck.class.getSimpleName(); @@ -26,8 +25,8 @@ public class AccessoryPermissionCheck extends BroadcastReceiver { public AccessoryPermissionCheck() { Log.d(TAG, "AccessoryPermissionCheck entered"); UsbAccessory accessory = UsbUtil.getUsbAccessory(); - if (accessory == null) { - throw new IOIOException("No usb accessory found."); + if (accessory == null || !"IOIO".equals(accessory.getModel())) { + throw new IOIOException("IOIO board not found."); } UsbManager usbManager = UsbUtil.getUsbManager(); @@ -52,8 +51,10 @@ public AccessoryPermissionCheck() { public synchronized void onReceive(final Context context, Intent intent) { Log.d(TAG, "onReceive entered"); if (ACTION_USB_PERMISSION.equals(intent.getAction())) { - boolean permitted = UsbUtil.getUsbManager().hasPermission(UsbUtil.getUsbAccessory()); - final String message = "USB access " + (permitted ? "permitted" : "denied"); + UsbAccessory accessory = UsbUtil.getUsbAccessory(); + String version = accessory != null ? accessory.getVersion() : ""; + boolean permitted = UsbUtil.getUsbManager().hasPermission(accessory); + final String message = "IOIO board [" + version + "] access " + (permitted ? "permitted" : "denied"); final BroadcastReceiver receiver = this; new Handler(Looper.getMainLooper()).post(() -> { Toast.makeText(context, message, Toast.LENGTH_LONG).show(); diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/android/UsbConnection.java b/ioio/ioio/src/main/java/ioio/smallbasic/android/UsbConnection.java index 53db954..666231c 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/android/UsbConnection.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/android/UsbConnection.java @@ -126,7 +126,7 @@ private void close() { inputStream = null; outputStream = null; fileDescriptor = null; - } catch (java.io.IOException e) { + } catch (IOException e) { IOUtil.setError("Failed to close file descriptor: " + e); Log.e(TAG, "Failed to close file descriptor.", e); } From 37af67c59d481c07ce62713c7ffbd99c2864cc72 Mon Sep 17 00:00:00 2001 From: chrisws Date: Sun, 7 Apr 2024 08:09:04 +0930 Subject: [PATCH 033/131] IOIO: some refactoring to separate out common utility code --- configure.ac | 10 +- {ioio => include}/log.h | 0 include/proxy.h | 402 ++++++++++++++++++ ioio/Makefile.am | 4 +- ioio/ioio/build.gradle | 2 +- .../android/AccessoryPermissionCheck.java | 2 +- .../{IOIOLoader.java => ModuleLoader.java} | 12 +- .../java/ioio/smallbasic/android/UsbUtil.java | 4 +- ioio/main.cpp | 398 +---------------- 9 files changed, 430 insertions(+), 404 deletions(-) rename {ioio => include}/log.h (100%) create mode 100644 include/proxy.h rename ioio/ioio/src/main/java/ioio/smallbasic/android/{IOIOLoader.java => ModuleLoader.java} (53%) diff --git a/configure.ac b/configure.ac index ffa572f..90b2594 100644 --- a/configure.ac +++ b/configure.ac @@ -1,7 +1,7 @@ dnl dnl Configure script for SmallBASIC plugins dnl -dnl Copyright(C) 2001-2021 Chris Warren-Smith. +dnl Copyright(C) 2001-2024 Chris Warren-Smith. dnl dnl This program is distributed under the terms of the GPL v2.0 dnl Download the GNU Public License (GPL) from www.gnu.org @@ -74,8 +74,8 @@ case "${host_os}" in GTK_SERVER_LDFLAGS="`pkg-config --libs gtk+-3.0` -lXm -lXt" GTK_SERVER_CPPFLAGS="`pkg-config --cflags gtk+-3.0` -DGTK_SERVER_FFI -DGTK_SERVER_LIBRARY -DGTK_SERVER_UNIX -DGTK_SERVER_GTK3x" RAYLIB_LDFLAGS="-lGL -lm -lpthread -ldl -lrt -lX11" - IOIO_CPPFLAGS="-I/usr/lib/jvm/java-1.8.0-openjdk-amd64/include -I/usr/lib/jvm/java-1.8.0-openjdk-amd64/include/linux" - IOIO_LDFLAGS="-L/usr/lib/jvm/java-1.8.0-openjdk-amd64/jre/lib/amd64/server -ljvm" + JVM_CPPFLAGS="-I/usr/lib/jvm/java-1.8.0-openjdk-amd64/include -I/usr/lib/jvm/java-1.8.0-openjdk-amd64/include/linux" + JVM_LDFLAGS="-L/usr/lib/jvm/java-1.8.0-openjdk-amd64/jre/lib/amd64/server -ljvm" esac AC_SUBST(DEBUG_LDFLAGS) @@ -86,8 +86,8 @@ AC_SUBST(WEBSOCKET_LDFLAGS) AC_SUBST(PLATFORM_LDFLAGS) AC_SUBST(GTK_SERVER_LDFLAGS) AC_SUBST(GTK_SERVER_CPPFLAGS) -AC_SUBST(IOIO_LDFLAGS) -AC_SUBST(IOIO_CPPFLAGS) +AC_SUBST(JVM_LDFLAGS) +AC_SUBST(JVM_CPPFLAGS) dnl change default aru setting to avoid warning ARFLAGS=cr diff --git a/ioio/log.h b/include/log.h similarity index 100% rename from ioio/log.h rename to include/log.h diff --git a/include/proxy.h b/include/proxy.h new file mode 100644 index 0000000..8b71dad --- /dev/null +++ b/include/proxy.h @@ -0,0 +1,402 @@ +// This file is part of SmallBASIC +// +// Copyright(C) 2024 Chris Warren-Smith. +// +// This program is distributed under the terms of the GPL v2.0 or later +// Download the GNU Public License (GPL) from www.gnu.org +// + +#pragma once + +#include "config.h" +#include "include/var.h" +#include + +JNIEnv *g_env; +JavaVM *g_jvm; +jobject g_activity; + +#define ARRAY_SIZE 10 + +#if defined(ANDROID_MODULE) + #define attachCurrentThread() g_jvm->AttachCurrentThread(&g_env, nullptr) + #define detachCurrentThread() g_jvm->DetachCurrentThread() +#else + #define attachCurrentThread() + #define detachCurrentThread() +#endif + +#if defined(ANDROID_MODULE) +// +// calls MainActivity.findClass() to return the loaded jclass for path +// +jclass findClass(const char *path) { + jclass clazz = g_env->GetObjectClass(g_activity); + jmethodID methodId = g_env->GetMethodID(clazz, "findClass", "(Ljava/lang/String;)Ljava/lang/Class;"); + jstring className = g_env->NewStringUTF(path); + jclass result = (jclass)g_env->CallObjectMethod(g_activity, methodId, className); + g_env->DeleteLocalRef(className); + g_env->DeleteLocalRef(clazz); + return reinterpret_cast(g_env->NewGlobalRef(result)); +} + +jobject createInstance(jclass clazz) { + jmethodID constructor = g_env->GetMethodID(clazz, "", "()V"); + jobject result; + if (constructor != nullptr) { + result = g_env->NewObject(clazz, constructor); + result = reinterpret_cast(g_env->NewGlobalRef(result)); + } else { + result = nullptr; + } + return result; +} + +#else +jclass findClass(const char *path) { + return g_env->FindClass(path); +} + +jobject createInstance(jclass clazz) { + jmethodID constructor = g_env->GetMethodID(clazz, "", "()V"); + jobject result; + if (constructor != nullptr) { + result = g_env->NewObject(clazz, constructor); + } else { + result = nullptr; + } + return result; +} +#endif + +struct JavaProxy { + JavaProxy(): + _clazz(nullptr), + _instance(nullptr), + _array(nullptr) { + } + + virtual ~JavaProxy() { + attachCurrentThread(); + if (_array) { + g_env->DeleteLocalRef(_array); + } +#if defined(ANDROID_MODULE) + g_env->DeleteGlobalRef(_clazz); + g_env->DeleteGlobalRef(_instance); +#endif + detachCurrentThread(); + _clazz = nullptr; + _instance = nullptr; + _array = nullptr; + } + + bool create(const char *path, var_s *retval) { + bool result; + if (_instance != nullptr) { + error(retval, "Internal error - already constructed"); + result = false; + } else { + attachCurrentThread(); + _clazz = findClass(path); + if (_clazz != nullptr) { + _instance = createInstance(_clazz); + } + result = _instance != nullptr; + if (!result) { + checkException(retval); + } + detachCurrentThread(); + } + return result; + } + + bool checkException(var_s *retval) { + auto exc = g_env->ExceptionOccurred(); + if (exc) { + g_env->ExceptionDescribe(); + g_env->ExceptionClear(); + if (retval) { + jclass clazz = g_env->FindClass("java/lang/Throwable"); + jmethodID methodId = g_env->GetMethodID(clazz, "getMessage", "()Ljava/lang/String;"); + jstring jstr = (jstring) g_env->CallObjectMethod(exc, methodId); + const char *message = g_env->GetStringUTFChars(jstr, JNI_FALSE); + error(retval, message); + g_env->ReleaseStringUTFChars(jstr, message); + } + } + return exc; + } + + // boolean foo(void) + int invokeBoolVoid(const char *name, var_s *retval) { + int result = 0; + if (_instance != nullptr) { + jmethodID method = g_env->GetMethodID(_clazz, name, "()Z"); + int value = 0; + if (method != nullptr) { + value = g_env->CallBooleanMethod(_instance, method); + } + if (!checkException(retval)) { + v_setint(retval, value); + result = 1; + } + } + return result; + } + + // float foo(void) + int invokeFloatVoid(const char *name, var_s *retval) { + int result = 0; + if (_instance != nullptr) { + attachCurrentThread(); + jmethodID method = g_env->GetMethodID(_clazz, name, "()F"); + var_num_t value = 0; + if (method != nullptr) { + value = g_env->CallFloatMethod(_instance, method); + } + if (!checkException(retval)) { + v_setreal(retval, value); + result = 1; + } + detachCurrentThread(); + } + return result; + } + + // int foo(void) + int invokeIntVoid(const char *name, var_s *retval) { + int result = 0; + if (_instance != nullptr) { + attachCurrentThread(); + jmethodID method = g_env->GetMethodID(_clazz, name, "()I"); + int value = 0; + if (method != nullptr) { + value = g_env->CallIntMethod(_instance, method); + } + if (!checkException(retval)) { + v_setint(retval, value); + result = 1; + } + detachCurrentThread(); + } + return result; + } + + // void foo(boolean) + int invokeVoidBool(const char *name, int value, var_s *retval) { + int result = 0; + if (_instance != nullptr) { + attachCurrentThread(); + jmethodID method = g_env->GetMethodID(_clazz, name, "(Z)V"); + if (method != nullptr) { + g_env->CallVoidMethod(_instance, method, value); + } + if (!checkException(retval)) { + result = 1; + } + detachCurrentThread(); + } + return result; + } + + // void foo(float) + int invokeVoidFloat(const char *name, var_num_t value, var_s *retval) { + int result = 0; + if (_instance != nullptr) { + attachCurrentThread(); + jmethodID method = g_env->GetMethodID(_clazz, name, "()F"); + if (method != nullptr) { + g_env->CallVoidMethod(_instance, method, value); + } + if (!checkException(retval)) { + result = 1; + } + detachCurrentThread(); + } + return result; + } + + // void foo(int) + int invokeVoidInt(const char *name, int value, var_s *retval) { + int result = 0; + if (_instance != nullptr) { + attachCurrentThread(); + jmethodID method = g_env->GetMethodID(_clazz, name, "(I)V"); + if (method != nullptr) { + g_env->CallVoidMethod(_instance, method, value); + } + if (!checkException(retval)) { + result = 1; + } + detachCurrentThread(); + } + return result; + } + + // void foo(int, int) + int invokeVoidInt2(const char *name, int value1, int value2, var_s *retval) { + int result = 0; + if (_instance != nullptr) { + attachCurrentThread(); + jmethodID method = g_env->GetMethodID(_clazz, name, "(II)V"); + if (method != nullptr) { + g_env->CallVoidMethod(_instance, method, value1, value2); + } + if (!checkException(retval)) { + result = 1; + } + detachCurrentThread(); + } + return result; + } + + // void foo(int, int, int, int) + int invokeVoidInt4(const char *name, int value1, int value2, int value3, int value4, var_s *retval) { + int result = 0; + if (_instance != nullptr) { + attachCurrentThread(); + jmethodID method = g_env->GetMethodID(_clazz, name, "(IIII)V"); + if (method != nullptr) { + g_env->CallVoidMethod(_instance, method, value1, value2, value3, value4); + } + if (!checkException(retval)) { + result = 1; + } + detachCurrentThread(); + } + return result; + } + + // void foo(void) + int invokeVoidVoid(const char *name, var_s *retval) { + int result = 0; + if (_instance != nullptr) { + attachCurrentThread(); + jmethodID method = g_env->GetMethodID(_clazz, name, "()V"); + if (method != nullptr) { + g_env->CallVoidMethod(_instance, method); + } + if (!checkException(retval)) { + result = 1; + } + detachCurrentThread(); + } + return result; + } + + // int readWrite(int address, byte[] write) { + int invokeReadWrite(int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + if (_instance != nullptr) { + attachCurrentThread(); + jmethodID method = g_env->GetMethodID(_clazz, "readWrite", "(II[BI)J"); + var_int_t value = 0; + if (method != nullptr) { + auto address = get_param_int(argc, arg, 0, 0); + auto readBytes = get_param_int(argc, arg, 1, 2); + populateByteArray(argc, arg, 2); + value = g_env->CallIntMethod(_instance, method, address, readBytes, _array, argc - 1); + } + if (!checkException(retval)) { + v_setint(retval, value); + result = 1; + } + detachCurrentThread(); + } + return result; + } + + // int write(int address, byte[] write) { + int invokeWrite(int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + if (_instance != nullptr) { + attachCurrentThread(); + jmethodID method = g_env->GetMethodID(_clazz, "write", "(I[BI)V"); + if (method != nullptr) { + auto address = get_param_int(argc, arg, 0, 0); + populateByteArray(argc, arg, 1); + g_env->CallVoidMethod(_instance, method, address, _array, argc - 1); + } + if (!checkException(retval)) { + result = 1; + } + detachCurrentThread(); + } + return result; + } + + // populate the java byte array with the contents of the basic array + void populateByteArray(int argc, slib_par_t *params, int offset) { + if (!_array) { + _array = g_env->NewByteArray(ARRAY_SIZE); + } + jbyte *elements = g_env->GetByteArrayElements(_array, nullptr); + if ((argc - offset) == 1 && is_param_array(argc, params, offset)) { + // argument is an array (assume of ints) + var_s *array = params[offset].var_p; + int size = v_asize(array); + for (int i = 0; i < size && i < ARRAY_SIZE; i++) { + var_s *elem = v_elem(array, i); + elements[i] = v_is_type(elem, V_INT) ? elem->v.i : elem->v.n; + } + } else { + for (int i = offset, j = 0; i < argc && i < ARRAY_SIZE; i++, j++) { + elements[j] = get_param_int(argc, params, i, 0); + } + } + // make the changes available to the java side + g_env->ReleaseByteArrayElements(_array, elements, 0); + } + + protected: + jclass _clazz; + jobject _instance; + jbyteArray _array; +}; + +#if defined(ANDROID_MODULE) +// Stores the Android JavaVM reference +// +extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void* reserved) { + logEntered(); + g_jvm = vm; + + jint result; + if (g_jvm->GetEnv((void **)&g_env, JNI_VERSION_1_6) != JNI_OK) { + result = JNI_ERR; + } else { + result = JNI_VERSION_1_6; + } + logLeaving(); + return result; +} + +#else + +int createJVM(const char *arg1, const char *arg2) { + JavaVMInitArgs vm_args; + JavaVMOption options[6]; + options[0].optionString = (char *)"-Xrs"; + options[1].optionString = (char *)arg1; + options[2].optionString = (char *)arg2; + options[3].optionString = (char *)"-Xdebug"; + options[4].optionString = (char *)"-agentlib:jdwp=transport=dt_socket,server=y,address=5005,suspend=y"; + options[5].optionString = (char *)"-Xcheck:jni"; + vm_args.version = JNI_VERSION_1_8; +#if defined(DEBUG) + vm_args.nOptions = 6; +#else + vm_args.nOptions = 3; +#endif + vm_args.ignoreUnrecognized = 1; + vm_args.options = options; + int result = (JNI_CreateJavaVM(&g_jvm, (void **)&g_env, &vm_args) == JNI_OK && + g_jvm->AttachCurrentThread((void **)&g_env, nullptr) == JNI_OK); + if (!result) { + fprintf(stderr, "Failed to create JVM\n"); + } + return result; +} + +#endif diff --git a/ioio/Makefile.am b/ioio/Makefile.am index 98f25be..c2065a8 100644 --- a/ioio/Makefile.am +++ b/ioio/Makefile.am @@ -19,11 +19,11 @@ all-am: $(generated) CLEANFILES = $(generated) AM_CXXFLAGS=-fno-rtti -std=c++14 -AM_CPPFLAGS = -DDESKTOP_MODULE -I../include -Wall @IOIO_CPPFLAGS@ +AM_CPPFLAGS = -DDESKTOP_MODULE -I../include -Wall @JVM_CPPFLAGS@ lib_LTLIBRARIES = libioio.la libioio_la_SOURCES = ../include/param.cpp ../include/hashmap.cpp ../include/apiexec.cpp main.cpp $(generated) -libioio_la_LDFLAGS = -module -rpath '$(libdir)' @PLATFORM_LDFLAGS@ @IOIO_LDFLAGS@ +libioio_la_LDFLAGS = -module -rpath '$(libdir)' @PLATFORM_LDFLAGS@ @JVM_LDFLAGS@ $(generated): api.json mkapi.bas mkdoc.bas $(sbasic) mkapi.bas > $@ diff --git a/ioio/ioio/build.gradle b/ioio/ioio/build.gradle index 3a4762f..721497e 100644 --- a/ioio/ioio/build.gradle +++ b/ioio/ioio/build.gradle @@ -12,7 +12,7 @@ android { } } - namespace 'net.sourceforge.smallbasic.ioio' + namespace 'ioio.smallbasic' compileSdk 34 defaultConfig { diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionCheck.java b/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionCheck.java index 41f65db..3189bda 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionCheck.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionCheck.java @@ -32,7 +32,7 @@ public AccessoryPermissionCheck() { UsbManager usbManager = UsbUtil.getUsbManager(); if (!usbManager.hasPermission(accessory)) { new Handler(Looper.getMainLooper()).post(() -> { - Context context = IOIOLoader.getContext(); + Context context = ModuleLoader.getContext(); IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION); filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY - 1); context.registerReceiver(this, filter, Context.RECEIVER_NOT_EXPORTED); diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/android/IOIOLoader.java b/ioio/ioio/src/main/java/ioio/smallbasic/android/ModuleLoader.java similarity index 53% rename from ioio/ioio/src/main/java/ioio/smallbasic/android/IOIOLoader.java rename to ioio/ioio/src/main/java/ioio/smallbasic/android/ModuleLoader.java index 5233878..99b2161 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/android/IOIOLoader.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/android/ModuleLoader.java @@ -7,18 +7,18 @@ import ioio.lib.spi.Log; /** - * IOIOLoader - Invoked from the "app" to commence loading + * ModuleLoader - Invoked from the "app" to commence loading */ -public class IOIOLoader { - private static final String TAG = "IOIOLoader"; +public class ModuleLoader { + private static final String TAG = "ModuleLoader"; public static native void init(Long app); private static WeakReference context; - public IOIOLoader(Long activity, Context context) { + public ModuleLoader(Long activity, Context context) { super(); - Log.d(TAG, "IOIOLoader: " + activity); - IOIOLoader.context = new WeakReference<>(context); + Log.d(TAG, "ModuleLoader: " + activity); + ModuleLoader.context = new WeakReference<>(context); init(activity); } diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/android/UsbUtil.java b/ioio/ioio/src/main/java/ioio/smallbasic/android/UsbUtil.java index ed5161e..9ca96c2 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/android/UsbUtil.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/android/UsbUtil.java @@ -15,13 +15,13 @@ static ParcelFileDescriptor getParcelFileDescriptor() { } static UsbAccessory getUsbAccessory() { - UsbManager usbManager = (UsbManager) IOIOLoader.getContext().getSystemService(Context.USB_SERVICE); + UsbManager usbManager = (UsbManager) ModuleLoader.getContext().getSystemService(Context.USB_SERVICE); UsbAccessory[] accessories = usbManager.getAccessoryList(); return (accessories == null ? null : accessories[0]); } static UsbManager getUsbManager() { - Context activity = IOIOLoader.getContext(); + Context activity = ModuleLoader.getContext(); return (UsbManager) activity.getSystemService(Context.USB_SERVICE); } } diff --git a/ioio/main.cpp b/ioio/main.cpp index 012b1a4..52a3a73 100644 --- a/ioio/main.cpp +++ b/ioio/main.cpp @@ -6,32 +6,14 @@ // Copyright(C) 2024 Chris Warren-Smith #include "config.h" -#include "log.h" -#include "include/var.h" -#include "include/module.h" -#include "include/param.h" - #include #include -#include -#include #include "robin-hood-hashing/src/include/robin_hood.h" - -struct IOTask; - -JNIEnv *g_env; -JavaVM *g_jvm; -IOTask *g_ioioTask; -int g_nextId = 1; -jobject g_activity; - -#if defined(ANDROID_MODULE) - #define attachCurrentThread() g_jvm->AttachCurrentThread(&g_env, nullptr) - #define detachCurrentThread() g_jvm->DetachCurrentThread() -#else - #define attachCurrentThread() - #define detachCurrentThread() -#endif +#include "include/log.h" +#include "include/var.h" +#include "include/module.h" +#include "include/param.h" +#include "include/proxy.h" #define CLASS_ANALOGINPUT "ioio/smallbasic/AnalogInputImpl" #define CLASS_DIGITALINPUT "ioio/smallbasic/DigitalInputImpl" @@ -43,331 +25,9 @@ jobject g_activity; #define CLASS_SPIMASTER "ioio/smallbasic/SpiMasterImpl" #define CLASS_IOIO "ioio/smallbasic/IOIOImpl" #define CLASS_IOTASK_ID 1 -#define ARRAY_SIZE 10 - -#if defined(ANDROID_MODULE) -// -// calls MainActivity.findClass() to return the loaded jclass for path -// -jclass findClass(const char *path) { - jclass clazz = g_env->GetObjectClass(g_activity); - jmethodID methodId = g_env->GetMethodID(clazz, "findClass", "(Ljava/lang/String;)Ljava/lang/Class;"); - jstring className = g_env->NewStringUTF(path); - jclass result = (jclass)g_env->CallObjectMethod(g_activity, methodId, className); - g_env->DeleteLocalRef(className); - g_env->DeleteLocalRef(clazz); - return reinterpret_cast(g_env->NewGlobalRef(result)); -} - -jobject createInstance(jclass clazz) { - jmethodID constructor = g_env->GetMethodID(clazz, "", "()V"); - jobject result; - if (constructor != nullptr) { - result = g_env->NewObject(clazz, constructor); - result = reinterpret_cast(g_env->NewGlobalRef(result)); - } else { - result = nullptr; - } - return result; -} - -#else -jclass findClass(const char *path) { - return g_env->FindClass(path); -} - -jobject createInstance(jclass clazz) { - jmethodID constructor = g_env->GetMethodID(clazz, "", "()V"); - jobject result; - if (constructor != nullptr) { - result = g_env->NewObject(clazz, constructor); - } else { - result = nullptr; - } - return result; -} -#endif - -struct IOTask { - IOTask(): - _clazz(nullptr), - _instance(nullptr), - _array(nullptr) { - } - - virtual ~IOTask() { - attachCurrentThread(); - if (_array) { - g_env->DeleteLocalRef(_array); - } -#if defined(ANDROID_MODULE) - g_env->DeleteGlobalRef(_clazz); - g_env->DeleteGlobalRef(_instance); -#endif - detachCurrentThread(); - _clazz = nullptr; - _instance = nullptr; - _array = nullptr; - } - - bool create(const char *path, var_s *retval) { - bool result; - if (_instance != nullptr) { - error(retval, "Internal error - already constructed"); - result = false; - } else { - attachCurrentThread(); - _clazz = findClass(path); - if (_clazz != nullptr) { - _instance = createInstance(_clazz); - } - result = _instance != nullptr; - if (!result) { - checkException(retval); - } - detachCurrentThread(); - } - return result; - } - - bool checkException(var_s *retval) { - auto exc = g_env->ExceptionOccurred(); - if (exc) { - if (retval) { - g_env->ExceptionClear(); - jclass clazz = g_env->FindClass("java/lang/Throwable"); - jmethodID methodId = g_env->GetMethodID(clazz, "getMessage", "()Ljava/lang/String;"); - jstring jstr = (jstring) g_env->CallObjectMethod(exc, methodId); - const char *message = g_env->GetStringUTFChars(jstr, JNI_FALSE); - error(retval, message); - g_env->ReleaseStringUTFChars(jstr, message); - } else { - g_env->ExceptionDescribe(); - g_env->ExceptionClear(); - } - } - return exc; - } - - // boolean foo(void) - int invokeBoolVoid(const char *name, var_s *retval) { - int result = 0; - if (_instance != nullptr) { - jmethodID method = g_env->GetMethodID(_clazz, name, "()Z"); - int value = 0; - if (method != nullptr) { - value = g_env->CallBooleanMethod(_instance, method); - } - if (!checkException(retval)) { - v_setint(retval, value); - result = 1; - } - } - return result; - } - - // float foo(void) - int invokeFloatVoid(const char *name, var_s *retval) { - int result = 0; - if (_instance != nullptr) { - attachCurrentThread(); - jmethodID method = g_env->GetMethodID(_clazz, name, "()F"); - var_num_t value = 0; - if (method != nullptr) { - value = g_env->CallFloatMethod(_instance, method); - } - if (!checkException(retval)) { - v_setreal(retval, value); - result = 1; - } - detachCurrentThread(); - } - return result; - } - - // int foo(void) - int invokeIntVoid(const char *name, var_s *retval) { - int result = 0; - if (_instance != nullptr) { - attachCurrentThread(); - jmethodID method = g_env->GetMethodID(_clazz, name, "()I"); - int value = 0; - if (method != nullptr) { - value = g_env->CallIntMethod(_instance, method); - } - if (!checkException(retval)) { - v_setint(retval, value); - result = 1; - } - detachCurrentThread(); - } - return result; - } - - // void foo(boolean) - int invokeVoidBool(const char *name, int value, var_s *retval) { - int result = 0; - if (_instance != nullptr) { - attachCurrentThread(); - jmethodID method = g_env->GetMethodID(_clazz, name, "(Z)V"); - if (method != nullptr) { - g_env->CallVoidMethod(_instance, method, value); - } - if (!checkException(retval)) { - result = 1; - } - detachCurrentThread(); - } - return result; - } - - // void foo(float) - int invokeVoidFloat(const char *name, var_num_t value, var_s *retval) { - int result = 0; - if (_instance != nullptr) { - attachCurrentThread(); - jmethodID method = g_env->GetMethodID(_clazz, name, "()F"); - if (method != nullptr) { - g_env->CallVoidMethod(_instance, method, value); - } - if (!checkException(retval)) { - result = 1; - } - detachCurrentThread(); - } - return result; - } - - // void foo(int) - int invokeVoidInt(const char *name, int value, var_s *retval) { - int result = 0; - if (_instance != nullptr) { - attachCurrentThread(); - jmethodID method = g_env->GetMethodID(_clazz, name, "(I)V"); - if (method != nullptr) { - g_env->CallVoidMethod(_instance, method, value); - } - if (!checkException(retval)) { - result = 1; - } - detachCurrentThread(); - } - return result; - } - - // void foo(int, int) - int invokeVoidInt2(const char *name, int value1, int value2, var_s *retval) { - int result = 0; - if (_instance != nullptr) { - attachCurrentThread(); - jmethodID method = g_env->GetMethodID(_clazz, name, "(II)V"); - if (method != nullptr) { - g_env->CallVoidMethod(_instance, method, value1, value2); - } - if (!checkException(retval)) { - result = 1; - } - detachCurrentThread(); - } - return result; - } - - // void foo(int, int, int, int) - int invokeVoidInt4(const char *name, int value1, int value2, int value3, int value4, var_s *retval) { - int result = 0; - if (_instance != nullptr) { - attachCurrentThread(); - jmethodID method = g_env->GetMethodID(_clazz, name, "(IIII)V"); - if (method != nullptr) { - g_env->CallVoidMethod(_instance, method, value1, value2, value3, value4); - } - if (!checkException(retval)) { - result = 1; - } - detachCurrentThread(); - } - return result; - } - - // void foo(void) - int invokeVoidVoid(const char *name, var_s *retval) { - int result = 0; - if (_instance != nullptr) { - attachCurrentThread(); - jmethodID method = g_env->GetMethodID(_clazz, name, "()V"); - if (method != nullptr) { - g_env->CallVoidMethod(_instance, method); - } - if (!checkException(retval)) { - result = 1; - } - detachCurrentThread(); - } - return result; - } - // int readWrite(int address, byte[] write) { - int invokeReadWrite(int argc, slib_par_t *arg, var_s *retval) { - int result = 0; - if (_instance != nullptr) { - attachCurrentThread(); - jmethodID method = g_env->GetMethodID(_clazz, "readWrite", "(II[BI)J"); - var_int_t value = 0; - if (method != nullptr) { - auto address = get_param_int(argc, arg, 0, 0); - auto readBytes = get_param_int(argc, arg, 1, 2); - populateByteArray(argc, arg, 2); - value = g_env->CallIntMethod(_instance, method, address, readBytes, _array, argc - 1); - } - if (!checkException(retval)) { - v_setint(retval, value); - result = 1; - } - detachCurrentThread(); - } - return result; - } - - // int write(int address, byte[] write) { - int invokeWrite(int argc, slib_par_t *arg, var_s *retval) { - int result = 0; - if (_instance != nullptr) { - attachCurrentThread(); - jmethodID method = g_env->GetMethodID(_clazz, "write", "(I[BI)V"); - if (method != nullptr) { - auto address = get_param_int(argc, arg, 0, 0); - populateByteArray(argc, arg, 1); - g_env->CallVoidMethod(_instance, method, address, _array, argc - 1); - } - if (!checkException(retval)) { - result = 1; - } - detachCurrentThread(); - } - return result; - } - - // populate the java byte array with the contents of the basic array - void populateByteArray(int argc, slib_par_t *params, int offset) { - if (!_array) { - _array = g_env->NewByteArray(ARRAY_SIZE); - } - jbyte *elements = g_env->GetByteArrayElements(_array, nullptr); - if ((argc - offset) == 1 && is_param_array(argc, params, offset)) { - // argument is an array (assume of ints) - var_s *array = params[offset].var_p; - int size = v_asize(array); - for (int i = 0; i < size && i < ARRAY_SIZE; i++) { - var_s *elem = v_elem(array, i); - elements[i] = v_is_type(elem, V_INT) ? elem->v.i : elem->v.n; - } - } else { - for (int i = offset, j = 0; i < argc && i < ARRAY_SIZE; i++, j++) { - elements[j] = get_param_int(argc, params, i, 0); - } - } - // make the changes available to the java side - g_env->ReleaseByteArrayElements(_array, elements, 0); +struct IOTask : JavaProxy { + IOTask() : JavaProxy() { } int open(int pin, var_s *retval) { @@ -381,14 +41,11 @@ struct IOTask { int open4(int pin1, int pin2, int pin3, int pin4, var_s *retval) { return invokeVoidInt4("open", pin1, pin2, pin3, pin4, retval); } - - private: - jclass _clazz; - jobject _instance; - jbyteArray _array; }; robin_hood::unordered_map g_ioTaskMap; +IOTask *g_ioioTask; +int g_nextId = 1; static int get_io_class_id(var_s *map, var_s *retval) { int result = -1; @@ -494,23 +151,7 @@ SBLIB_API int sblib_func_count() { // int sblib_init(const char *sourceFile) { #if defined(DESKTOP_MODULE) - JavaVMInitArgs vm_args; - JavaVMOption options[3]; - options[0].optionString = (char *)"-Djava.class.path=./ioio-1.0-jar-with-dependencies.jar"; - options[1].optionString = (char *)"-Dioio.SerialPorts=IOIO0"; - options[2].optionString = (char *)"-Xrs"; - //options[2].optionString = "-Xdebug"; - //options[3].optionString = "-agentlib:jdwp=transport=dt_socket,server=y,address=5005,suspend=y"; - //options[2].optionString = (char *)"-Xcheck:jni"; - vm_args.version = JNI_VERSION_1_8; - vm_args.nOptions = 3; - vm_args.options = options; - vm_args.ignoreUnrecognized = 0; - int result = (JNI_CreateJavaVM(&g_jvm, (void **)&g_env, &vm_args) == JNI_OK && - g_jvm->AttachCurrentThread((void **)&g_env, nullptr) == JNI_OK); - if (!result) { - fprintf(stderr, "Failed to create JVM\n"); - } + int result = createJVM("-Djava.class.path=./ioio-1.0-jar-with-dependencies.jar", "-Dioio.SerialPorts=IOIO0"); #else int result = 1; #endif @@ -528,27 +169,10 @@ int sblib_init(const char *sourceFile) { } #if defined(ANDROID_MODULE) -// -// Stores the Android JavaVM reference -// -extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void* reserved) { - logEntered(); - g_jvm = vm; - - jint result; - if (g_jvm->GetEnv((void **)&g_env, JNI_VERSION_1_6) != JNI_OK) { - result = JNI_ERR; - } else { - result = JNI_VERSION_1_6; - } - logLeaving(); - return result; -} - // // Retrieves the _app->activity->clazz value sent from App/JNI to Java to IOIOLoader // -extern "C" JNIEXPORT void JNICALL Java_ioio_smallbasic_android_IOIOLoader_init +extern "C" JNIEXPORT void JNICALL Java_ioio_smallbasic_android_ModuleLoader_init (JNIEnv *env, jclass clazz, jobject activity) { logEntered(); jclass longClass = env->FindClass("java/lang/Long"); From 9348c3260073d4b59869c52851891ecf205fc164 Mon Sep 17 00:00:00 2001 From: chrisws Date: Sat, 13 Apr 2024 10:19:43 +0930 Subject: [PATCH 034/131] Update dependencies --- nuklear/Nuklear | 2 +- raylib/README.md | 4 ++-- raylib/func.h | 4 ++-- raylib/raygui | 2 +- raylib/raylib | 2 +- websocket/mongoose | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/nuklear/Nuklear b/nuklear/Nuklear index 055a0aa..31b5729 160000 --- a/nuklear/Nuklear +++ b/nuklear/Nuklear @@ -1 +1 @@ -Subproject commit 055a0aad0ff10921d638a9b93b0fe87f3a86d777 +Subproject commit 31b5729d66403e4ca1b6ff6df2958c49f3bc2c8d diff --git a/raylib/README.md b/raylib/README.md index 64b9393..e4d9d64 100644 --- a/raylib/README.md +++ b/raylib/README.md @@ -627,8 +627,8 @@ Unimplemented APIs | Name | Description | |---------|---------------| -| AttachAudioMixedProcessor | Attach audio stream processor to the entire audio pipeline, receives the samples as s | -| AttachAudioStreamProcessor | Attach audio stream processor to stream, receives the samples as s | +| AttachAudioMixedProcessor | Attach audio stream processor to the entire audio pipeline, receives the samples as 'float' | +| AttachAudioStreamProcessor | Attach audio stream processor to stream, receives the samples as 'float' | | BeginVrStereoMode | Begin stereo rendering (requires VR simulator) | | DetachAudioMixedProcessor | Detach audio stream processor from the entire audio pipeline | | DetachAudioStreamProcessor | Detach audio stream processor from stream | diff --git a/raylib/func.h b/raylib/func.h index 867e354..0432151 100644 --- a/raylib/func.h +++ b/raylib/func.h @@ -1534,8 +1534,8 @@ static int cmd_getscreentoworldray(int argc, slib_par_t *params, var_t *retval) static int cmd_getscreentoworldrayex(int argc, slib_par_t *params, var_t *retval) { auto position = get_param_vec2(argc, params, 0); auto camera = get_camera_3d(argc, params, 1); - auto width = get_param_num(argc, params, 2, 0); - auto height = get_param_num(argc, params, 3, 0); + auto width = get_param_int(argc, params, 2, 0); + auto height = get_param_int(argc, params, 3, 0); auto fnResult = GetScreenToWorldRayEx(position, camera, width, height); v_setray(retval, fnResult); return 1; diff --git a/raylib/raygui b/raylib/raygui index 9060e3b..f21318f 160000 --- a/raylib/raygui +++ b/raylib/raygui @@ -1 +1 @@ -Subproject commit 9060e3bf33795dad4fdb4f60585b07901ffa5dee +Subproject commit f21318f5008e2d4b1a4980b7fd22b3885206981a diff --git a/raylib/raylib b/raylib/raylib index 9cf408f..b3dfa2d 160000 --- a/raylib/raylib +++ b/raylib/raylib @@ -1 +1 @@ -Subproject commit 9cf408f77c58828fd1019d5cd35c6b8c5139c8dc +Subproject commit b3dfa2d8abff549a9a9129f7ef2462ef1261ac4d diff --git a/websocket/mongoose b/websocket/mongoose index d7b2b3f..be5d8b1 160000 --- a/websocket/mongoose +++ b/websocket/mongoose @@ -1 +1 @@ -Subproject commit d7b2b3f42ec1109e6955baff6f836a15cfdd107a +Subproject commit be5d8b1d4f3b196ae0ee8e66525e743ed587d35f From 8de0a9b3c14f38b22703faa6072bb74ad8364b9c Mon Sep 17 00:00:00 2001 From: chrisws Date: Mon, 22 Apr 2024 20:38:03 +0930 Subject: [PATCH 035/131] Update dependencies --- raylib/README.md | 5 +++-- raylib/proc-def.h | 3 ++- raylib/proc.h | 16 ++++++++++++++-- raylib/raygui | 2 +- raylib/raylib | 2 +- websocket/main.cpp | 8 ++++---- websocket/mongoose | 2 +- 7 files changed, 26 insertions(+), 12 deletions(-) diff --git a/raylib/README.md b/raylib/README.md index e4d9d64..2d24b43 100644 --- a/raylib/README.md +++ b/raylib/README.md @@ -4,7 +4,7 @@ raylib is a simple and easy-to-use library to enjoy videogames programming. https://www.raylib.com/ -Implemented APIs (612) +Implemented APIs (613) ---------------- | Name | Description | @@ -109,7 +109,8 @@ Implemented APIs (612) | sub DrawRectanglePro(rec, origin, rotation, color) | Draw a color-filled rectangle with pro parameters | | sub DrawRectangleRec(rec, color) | Draw a color-filled rectangle | | sub DrawRectangleRounded(rec, roundness, segments, color) | Draw rectangle with rounded edges | -| sub DrawRectangleRoundedLines(rec, roundness, segments, lineThick, color) | Draw rectangle with rounded edges outline | +| sub DrawRectangleRoundedLines(rec, roundness, segments, color) | Draw rectangle lines with rounded edges | +| sub DrawRectangleRoundedLinesEx(rec, roundness, segments, lineThick, color) | Draw rectangle with rounded edges outline | | sub DrawRectangleV(position, size, color) | Draw a color-filled rectangle (Vector version) | | sub DrawRing(center, innerRadius, outerRadius, startAngle, endAngle, segments, color) | Draw ring | | sub DrawRingLines(center, innerRadius, outerRadius, startAngle, endAngle, segments, color) | Draw ring outline | diff --git a/raylib/proc-def.h b/raylib/proc-def.h index 36185b6..470380a 100644 --- a/raylib/proc-def.h +++ b/raylib/proc-def.h @@ -64,7 +64,8 @@ {4, 4, "DRAWRECTANGLEPRO", cmd_drawrectanglepro}, {2, 2, "DRAWRECTANGLEREC", cmd_drawrectanglerec}, {4, 4, "DRAWRECTANGLEROUNDED", cmd_drawrectanglerounded}, - {5, 5, "DRAWRECTANGLEROUNDEDLINES", cmd_drawrectangleroundedlines}, + {4, 4, "DRAWRECTANGLEROUNDEDLINES", cmd_drawrectangleroundedlines}, + {5, 5, "DRAWRECTANGLEROUNDEDLINESEX", cmd_drawrectangleroundedlinesex}, {3, 3, "DRAWRECTANGLEV", cmd_drawrectanglev}, {7, 7, "DRAWRING", cmd_drawring}, {7, 7, "DRAWRINGLINES", cmd_drawringlines}, diff --git a/raylib/proc.h b/raylib/proc.h index dbe11b3..45d7227 100644 --- a/raylib/proc.h +++ b/raylib/proc.h @@ -820,15 +820,27 @@ static int cmd_drawrectanglerounded(int argc, slib_par_t *params, var_t *retval) } // -// Draw rectangle with rounded edges outline +// Draw rectangle lines with rounded edges // static int cmd_drawrectangleroundedlines(int argc, slib_par_t *params, var_t *retval) { + auto rec = get_param_rect(argc, params, 0); + auto roundness = get_param_num(argc, params, 1, 0); + auto segments = get_param_int(argc, params, 2, 0); + auto color = get_param_color(argc, params, 3); + DrawRectangleRoundedLines(rec, roundness, segments, color); + return 1; +} + +// +// Draw rectangle with rounded edges outline +// +static int cmd_drawrectangleroundedlinesex(int argc, slib_par_t *params, var_t *retval) { auto rec = get_param_rect(argc, params, 0); auto roundness = get_param_num(argc, params, 1, 0); auto segments = get_param_int(argc, params, 2, 0); auto lineThick = get_param_num(argc, params, 3, 0); auto color = get_param_color(argc, params, 4); - DrawRectangleRoundedLines(rec, roundness, segments, lineThick, color); + DrawRectangleRoundedLinesEx(rec, roundness, segments, lineThick, color); return 1; } diff --git a/raylib/raygui b/raylib/raygui index f21318f..6f53233 160000 --- a/raylib/raygui +++ b/raylib/raygui @@ -1 +1 @@ -Subproject commit f21318f5008e2d4b1a4980b7fd22b3885206981a +Subproject commit 6f532337ff2d6dc14ba3a5f5b7ee63f9d90ff13b diff --git a/raylib/raylib b/raylib/raylib index b3dfa2d..e0f6faa 160000 --- a/raylib/raylib +++ b/raylib/raylib @@ -1 +1 @@ -Subproject commit b3dfa2d8abff549a9a9129f7ef2462ef1261ac4d +Subproject commit e0f6faa151589a185a04c2c723c01daff1b0a78f diff --git a/websocket/main.cpp b/websocket/main.cpp index 1d7c1cf..a794e57 100644 --- a/websocket/main.cpp +++ b/websocket/main.cpp @@ -78,7 +78,7 @@ static void send(mg_connection *conn, const char *msg, int len) { } static void server_http_msg(mg_connection *conn, mg_http_message *message, Session *session) { - if (mg_http_match_uri(message, "/")) { + if (mg_match(message->uri, mg_str("/"), NULL)) { mg_ws_upgrade(conn, message, NULL); session->_conns.push_back(new Session(conn)); } else { @@ -103,7 +103,7 @@ static void server_send(Session *session, const char *buf, size_t len, int id) { static void server_ws_msg(mg_connection *conn, mg_ws_message *message) { Session *session = sessions[conn->id]; if (session != nullptr) { - session->_recv.append((char *)message->data.ptr, message->data.len); + session->_recv.append((char *)message->data.buf, message->data.len); } mg_iobuf_del(&conn->recv, 0, conn->recv.len); } @@ -164,9 +164,9 @@ static void client_ws_open(mg_connection *conn, Session *session) { static void client_ws_msg(mg_connection *conn, mg_ws_message *message, Session *session) { if (message->data.len && session->_state == kClient) { if (!session->_recv.empty()) { - session->_recv.insert(0, (char *)message->data.ptr, message->data.len); + session->_recv.insert(0, (char *)message->data.buf, message->data.len); } else { - session->_recv.append((char *)message->data.ptr, message->data.len); + session->_recv.append((char *)message->data.buf, message->data.len); } } } diff --git a/websocket/mongoose b/websocket/mongoose index be5d8b1..59020e0 160000 --- a/websocket/mongoose +++ b/websocket/mongoose @@ -1 +1 @@ -Subproject commit be5d8b1d4f3b196ae0ee8e66525e743ed587d35f +Subproject commit 59020e0d5db10b453fb688daaf515bae6e9f582d From 89b6d21e7c6faa8c1c961ee4ea2594bac4c091af Mon Sep 17 00:00:00 2001 From: chrisws Date: Mon, 22 Apr 2024 20:49:17 +0930 Subject: [PATCH 036/131] Fix windows build regression --- configure.ac | 3 +++ nuklear/Makefile.am | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 90b2594..2c7512a 100644 --- a/configure.ac +++ b/configure.ac @@ -64,6 +64,7 @@ case "${host_os}" in GTK_SERVER_CPPFLAGS="-DGTK_SERVER_WIN32" IOIO_CPPFLAGS="" IOIO_LDFLAGS="" + NUKLEAR_CPPFLAGS="" ;; *) @@ -76,6 +77,7 @@ case "${host_os}" in RAYLIB_LDFLAGS="-lGL -lm -lpthread -ldl -lrt -lX11" JVM_CPPFLAGS="-I/usr/lib/jvm/java-1.8.0-openjdk-amd64/include -I/usr/lib/jvm/java-1.8.0-openjdk-amd64/include/linux" JVM_LDFLAGS="-L/usr/lib/jvm/java-1.8.0-openjdk-amd64/jre/lib/amd64/server -ljvm" + NUKLEAR_CPPFLAGS="-D_GLFW_X11=1" esac AC_SUBST(DEBUG_LDFLAGS) @@ -88,6 +90,7 @@ AC_SUBST(GTK_SERVER_LDFLAGS) AC_SUBST(GTK_SERVER_CPPFLAGS) AC_SUBST(JVM_LDFLAGS) AC_SUBST(JVM_CPPFLAGS) +AC_SUBST(NUKLEAR_CPPFLAGS) dnl change default aru setting to avoid warning ARFLAGS=cr diff --git a/nuklear/Makefile.am b/nuklear/Makefile.am index c924e99..5b4f06f 100644 --- a/nuklear/Makefile.am +++ b/nuklear/Makefile.am @@ -6,7 +6,7 @@ # AM_CXXFLAGS=-fno-rtti -std=c++14 -AM_CPPFLAGS = -D_GLFW_BUILD_DLL=1 -D_GLFW_X11=1 \ +AM_CPPFLAGS = -D_GLFW_BUILD_DLL=1 @NUKLEAR_CPPFLAGS@ \ -I../raylib/raylib/src/external/glfw/include \ -I../raylib/raylib/src/external/glfw/deps lib_LTLIBRARIES = libnuklear.la From 94b4e8886e8342b514349459702612bc77a895d5 Mon Sep 17 00:00:00 2001 From: Joerg Siebenmorgen Date: Thu, 25 Apr 2024 21:31:38 +0200 Subject: [PATCH 037/131] IOIO example push button --- ioio/samples/button.bas | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 ioio/samples/button.bas diff --git a/ioio/samples/button.bas b/ioio/samples/button.bas new file mode 100644 index 0000000..79c770d --- /dev/null +++ b/ioio/samples/button.bas @@ -0,0 +1,41 @@ +' PUSH BUTTON +' =========== +' +' This example demonstrates how to connect a push button +' and read the state of this button. If the button is pressed +' a 0 will be returned otherwise 1 +' +' --------------- +' PIN10 o----| Push Button |----o GND +' --------------- +' +' The push button is connected to pin 10 and to GND of the IOIO board. + + +import ioio + +PIN10 = ioio.openDigitalInput(10) + + +print "Wait for connection to IOIO board" +ioio.waitForConnect(10) +print "Connection established" +print +print "Press button connected to IOIO board or q for quit" + +isRunning = true + +while(isRunning) + + key = inkey() + if(key == "q") then isRunning = false + + value = PIN10.read() + + locate(6,0): print "Button value is: " + value + + delay(50) + +wend + +print "done" From 0a74486934291e16975555dc943046081699e997 Mon Sep 17 00:00:00 2001 From: chrisws Date: Thu, 2 May 2024 21:10:57 +0930 Subject: [PATCH 038/131] Fix to allow openPwmOutput to take second frequency argument #8 --- include/{proxy.h => javaproxy.h} | 2 +- ioio/api.json | 1 + .../pc/SerialPortIOIOConnection.java | 27 +++++++++---------- .../pc/SerialPortIOIOConnectionBootstrap.java | 9 +++---- ioio/main.cpp | 4 +-- ioio/samples/pwm.bas | 14 ++++++++++ 6 files changed, 35 insertions(+), 22 deletions(-) rename include/{proxy.h => javaproxy.h} (99%) create mode 100644 ioio/samples/pwm.bas diff --git a/include/proxy.h b/include/javaproxy.h similarity index 99% rename from include/proxy.h rename to include/javaproxy.h index 8b71dad..9f08b51 100644 --- a/include/proxy.h +++ b/include/javaproxy.h @@ -205,7 +205,7 @@ struct JavaProxy { int result = 0; if (_instance != nullptr) { attachCurrentThread(); - jmethodID method = g_env->GetMethodID(_clazz, name, "()F"); + jmethodID method = g_env->GetMethodID(_clazz, name, "(F)V"); if (method != nullptr) { g_env->CallVoidMethod(_instance, method, value); } diff --git a/ioio/api.json b/ioio/api.json index bc77970..4ba5b79 100644 --- a/ioio/api.json +++ b/ioio/api.json @@ -229,6 +229,7 @@ { "name": "PwmOutput", "comment": "A pin used for PWM (Pulse-Width Modulation) output. A PWM pin produces a logic-level PWM signal. These signals are typically used for simulating analog outputs for controlling the intensity of LEDs, the rotation speed of motors, etc. They are also frequently used for controlling hobby servo motors. PwmOutput instances are obtained by calling IOIO#openPwmOutput. When used for motors and LEDs, a frequency of several KHz is typically used, where there is a trade-off between switching power-loses and smoothness of operation. The pulse width is typically set by specifying the duty cycle, with the setDutyCycle method. A duty cycle of 0 is \"off\", a duty cycle of 1 is \"on\", and every intermediate value produces an intermediate intensity. Please note that any devices consuming more than 20mA of current (e.g. motors) should not by directly connected the the IOIO pins, but rather through an amplification circuit suited for the specific load. When used for hobby servos, the PWM signal is rather used for encoding of the desired angle the motor should go to. By standard, a 100Hz signal is used and the pulse width is varied between 1ms and 2ms (corresponding to both extremes of the shaft angle), using setPulseWidth. The instance is alive since its creation. If the connection with the IOIO drops at any point, the instance transitions to a disconnected state, in which every attempt to use the pin (except close()) will throw a ConnectionLostException. Whenever close() is invoked the instance may no longer be used. Any resources associated with it are freed and can be reused. Typical usage (fading LED):", + "pins": 2, "methods": [ { "name": "setDutyCycle", diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/pc/SerialPortIOIOConnection.java b/ioio/ioio/src/main/java/ioio/smallbasic/pc/SerialPortIOIOConnection.java index a98148c..23fcda4 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/pc/SerialPortIOIOConnection.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/pc/SerialPortIOIOConnection.java @@ -1,15 +1,14 @@ package ioio.smallbasic.pc; import com.fazecast.jSerialComm.SerialPort; +import ioio.lib.api.IOIOConnection; +import ioio.lib.api.exception.ConnectionLostException; +import ioio.lib.spi.Log; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import ioio.lib.api.IOIOConnection; -import ioio.lib.api.exception.ConnectionLostException; -import ioio.lib.spi.Log; - public class SerialPortIOIOConnection implements IOIOConnection { private static final String TAG = "SerialPortIOIOConnection"; private static final int READ_TIMEOUT_MILLIS = 20000; @@ -27,14 +26,8 @@ public SerialPortIOIOConnection(String portName) { } @Override - public void waitForConnect() throws ConnectionLostException { - if (!abort && serialPort.openPort()) { - inputStream = serialPort.getInputStream(); - outputStream = serialPort.getOutputStream(); - serialPort.setDTR(); - } else { - throw new ConnectionLostException(); - } + public boolean canClose() { + return serialPort.isOpen(); } @Override @@ -67,7 +60,13 @@ public OutputStream getOutputStream() throws ConnectionLostException { } @Override - public boolean canClose() { - return true; + public void waitForConnect() throws ConnectionLostException { + if (!abort && serialPort.openPort()) { + inputStream = serialPort.getInputStream(); + outputStream = serialPort.getOutputStream(); + serialPort.setDTR(); + } else { + throw new ConnectionLostException(); + } } } diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/pc/SerialPortIOIOConnectionBootstrap.java b/ioio/ioio/src/main/java/ioio/smallbasic/pc/SerialPortIOIOConnectionBootstrap.java index 19f4bef..eaec883 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/pc/SerialPortIOIOConnectionBootstrap.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/pc/SerialPortIOIOConnectionBootstrap.java @@ -30,6 +30,10 @@ package ioio.smallbasic.pc; import com.fazecast.jSerialComm.SerialPort; +import ioio.lib.api.IOIOConnection; +import ioio.lib.spi.IOIOConnectionBootstrap; +import ioio.lib.spi.IOIOConnectionFactory; +import ioio.lib.spi.Log; import java.util.Arrays; import java.util.Collection; @@ -37,11 +41,6 @@ import java.util.LinkedList; import java.util.List; -import ioio.lib.api.IOIOConnection; -import ioio.lib.spi.IOIOConnectionBootstrap; -import ioio.lib.spi.IOIOConnectionFactory; -import ioio.lib.spi.Log; - public class SerialPortIOIOConnectionBootstrap implements IOIOConnectionBootstrap { private static final String TAG = "SerialPortIOIOConnectionBootstrap"; diff --git a/ioio/main.cpp b/ioio/main.cpp index 52a3a73..55a6e89 100644 --- a/ioio/main.cpp +++ b/ioio/main.cpp @@ -13,7 +13,7 @@ #include "include/var.h" #include "include/module.h" #include "include/param.h" -#include "include/proxy.h" +#include "include/javaproxy.h" #define CLASS_ANALOGINPUT "ioio/smallbasic/AnalogInputImpl" #define CLASS_DIGITALINPUT "ioio/smallbasic/DigitalInputImpl" @@ -122,7 +122,7 @@ FUNC_SIG lib_func[] = { {1, 1, "OPENDIGITALINPUT", cmd_opendigitalinput}, {1, 1, "OPENDIGITALOUTPUT", cmd_opendigitaloutput}, {1, 1, "OPENPULSEINPUT", cmd_openpulseinput}, - {1, 1, "OPENPWMOUTPUT", cmd_openpwmoutput}, + {2, 2, "OPENPWMOUTPUT", cmd_openpwmoutput}, {2, 2, "OPENTWIMASTER", cmd_opentwimaster}, {4, 4, "OPENSPIMASTER", cmd_openspimaster}, }; diff --git a/ioio/samples/pwm.bas b/ioio/samples/pwm.bas new file mode 100644 index 0000000..e6635fc --- /dev/null +++ b/ioio/samples/pwm.bas @@ -0,0 +1,14 @@ +import ioio + +led = ioio.openPwmOutput(46, 99) + +print "wait for connect" +ioio.waitForConnect(10) +print "ready!!!" + +for DutyCycle = 0 to 1 step 0.1 + led.setDutyCycle(DutyCycle) + delay 100 +next + +print "Done" From 103854902bb7e79acb5826d78e94bd61e2516a5e Mon Sep 17 00:00:00 2001 From: Joerg Siebenmorgen Date: Sun, 12 May 2024 14:37:19 +0200 Subject: [PATCH 039/131] Add examples --- ioio/README.md | 8 +++++-- ioio/samples/bh1750.bas | 46 +++++++++++++++++++++++++++++++++++++++++ ioio/samples/pwm.bas | 20 +++++++++++++++--- 3 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 ioio/samples/bh1750.bas diff --git a/ioio/README.md b/ioio/README.md index 5d1bcd9..25c97ff 100644 --- a/ioio/README.md +++ b/ioio/README.md @@ -91,7 +91,7 @@ A pin used for digital output. A digital output pin can be used to generate logi A pin used for PWM (Pulse-Width Modulation) output. A PWM pin produces a logic-level PWM signal. These signals are typically used for simulating analog outputs for controlling the intensity of LEDs, the rotation speed of motors, etc. They are also frequently used for controlling hobby servo motors. PwmOutput instances are obtained by calling IOIO#openPwmOutput. When used for motors and LEDs, a frequency of several KHz is typically used, where there is a trade-off between switching power-loses and smoothness of operation. The pulse width is typically set by specifying the duty cycle, with the setDutyCycle method. A duty cycle of 0 is \"off\", a duty cycle of 1 is \"on\", and every intermediate value produces an intermediate intensity. Please note that any devices consuming more than 20mA of current (e.g. motors) should not by directly connected the the IOIO pins, but rather through an amplification circuit suited for the specific load. When used for hobby servos, the PWM signal is rather used for encoding of the desired angle the motor should go to. By standard, a 100Hz signal is used and the pulse width is varied between 1ms and 2ms (corresponding to both extremes of the shaft angle), using setPulseWidth. The instance is alive since its creation. If the connection with the IOIO drops at any point, the instance transitions to a disconnected state, in which every attempt to use the pin (except close()) will throw a ConnectionLostException. Whenever close() is invoked the instance may no longer be used. Any resources associated with it are freed and can be reused. Typical usage (fading LED): -`io = ioio.openPwmOutput(pin)` +`io = ioio.openPwmOutput(pin, frequency)` | Name | Description | |---------|---------------| @@ -102,10 +102,14 @@ A pin used for PWM (Pulse-Width Modulation) output. A PWM pin produces a logic-l An interface for controlling a TWI module, in TWI bus-master mode, enabling communication with multiple TWI-enabled slave modules. -`io = ioio.openTwiMaster(pin)` +`io = ioio.openTwiMaster(TWINumber, mode)` + +Opens a TWI module number TWINumber in master mode, using its dedicated SDA and SCL pins. The TWI module will run at 100KHz and will use I2C voltage levels if mode = false (pass true for SMBus levels). | Name | Description | |---------|---------------| +|void write(Address, Register, DataByte) | Writes a byte of data to the given register of an I2C device with given address.| +|int readwrite(Address, NumReceiveBytes, Register, DataByte) | Writes a byte of data to the given register of an I2C deice with address and reads NumReceiveBytes. NumReceiveBytes can be max 8 bytes long.| ## SpiMaster diff --git a/ioio/samples/bh1750.bas b/ioio/samples/bh1750.bas new file mode 100644 index 0000000..56f771c --- /dev/null +++ b/ioio/samples/bh1750.bas @@ -0,0 +1,46 @@ +' BH1750 - Ambient light sensor +' ============================= +' +' This examample demonstrates how to measure the ambient light luminous flux +' using the BH1750 I2C sensor. +' +' Connect the sensor to the IOIO-OTG board: +' +' ------ ------ +' IOIO | |BH1750 +' PIN 4|-------|SDA +' PIN 5|-------|SCL +' GND |-------|GND +' 3.3V |-------|VIN +' | |ADDR +'------- ------ + +' If ADDR is not connected, 0x23 as I2C address will be used. +' + +import ioio + +const ADDRESS = 0x23 + +Print "Connect to BH1750" +sensor = ioio.openTwiMaster(0, 0) +ioio.waitForConnect(10) +Print "Connection established" + +' Power down +sensor.write(ADDRESS, 0x00) +' Power on +sensor.write(ADDRESS, 0x01) +delay(1000) + +' Read one time with low resolution +ValueLowRes = sensor.readwrite(ADDRESS, 2, 0x23) / 1.2 +delay(1000) +' Read one time with high resolution +ValueHighRes = sensor.readwrite(ADDRESS, 2, 0x20) / 1.2 + +print "Low resolution : " + ValueLowRes + " lx" +print "High resolution: " + valueHighRes + " lx" + + + \ No newline at end of file diff --git a/ioio/samples/pwm.bas b/ioio/samples/pwm.bas index e6635fc..c6ad84c 100644 --- a/ioio/samples/pwm.bas +++ b/ioio/samples/pwm.bas @@ -1,14 +1,28 @@ +' PWM with a LED +' ============================= +' +' This example demonstrates how to use PWN to +' control the brightness of a LED +' +' Connect a LED and a resistor as following: +' +' |\ | +' IOIO | \| -------- IOIO +' PIN 46 ---| |---| R = 1k |--- GND +' | /| -------- +' |/ | + import ioio -led = ioio.openPwmOutput(46, 99) +led = ioio.openPwmOutput(46, 1000) print "wait for connect" ioio.waitForConnect(10) print "ready!!!" -for DutyCycle = 0 to 1 step 0.1 +for DutyCycle = 0 to 1 step 0.01 led.setDutyCycle(DutyCycle) - delay 100 + delay 10 next print "Done" From dac78d191b48ca006cb7292037cdb82a3aaf9953 Mon Sep 17 00:00:00 2001 From: Joerg Siebenmorgen <82510480+Joe7M@users.noreply.github.com> Date: Sun, 12 May 2024 14:39:58 +0200 Subject: [PATCH 040/131] Update README.md --- ioio/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ioio/README.md b/ioio/README.md index 25c97ff..5f40b01 100644 --- a/ioio/README.md +++ b/ioio/README.md @@ -109,7 +109,7 @@ Opens a TWI module number TWINumber in master mode, using its dedicated SDA and | Name | Description | |---------|---------------| |void write(Address, Register, DataByte) | Writes a byte of data to the given register of an I2C device with given address.| -|int readwrite(Address, NumReceiveBytes, Register, DataByte) | Writes a byte of data to the given register of an I2C deice with address and reads NumReceiveBytes. NumReceiveBytes can be max 8 bytes long.| +|int readwrite(Address, NumReceiveBytes, Register, DataByte) | Writes a byte of data to the given register of an I2C device with given address and reads NumReceiveBytes. NumReceiveBytes can be max 8 bytes long.| ## SpiMaster From 5bbcd0f697ceacc9c46c286ef5b0858fbd9357b1 Mon Sep 17 00:00:00 2001 From: Joerg Siebenmorgen Date: Mon, 27 May 2024 22:00:21 +0200 Subject: [PATCH 041/131] Add ST7789 example --- ioio/samples/st7789.bas | 220 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 ioio/samples/st7789.bas diff --git a/ioio/samples/st7789.bas b/ioio/samples/st7789.bas new file mode 100644 index 0000000..21983d5 --- /dev/null +++ b/ioio/samples/st7789.bas @@ -0,0 +1,220 @@ +' ST7789 - TFT controller +' ============================= +' +' This example demonstrates how to drive a TFT display with a ST7789 controller. +' With the current IOIO implementation the example works but is unusable slow. +' ---------------------------------- +' There exist many TFT displays using the ST7789 controller. This examples is written +' for the Waveshare 1.3inch LCD module with 240x240 pixels. With some minor modification +' especially for the pins, TFTs from other manufacturers should also work. Be carefull +' with VCC. If you are using a bare TFT, then drive it only with 3.3V. Many breakout +' boards (i.e. Adafruit) support 5V. +' +' ------- ------ +' IOIO | |TFT +' PIN 40|-------|DIN (MOSI) +' PIN 39|-------|CLK (SCL) +' PIN 38|-------|CS +' PIN 37|-------|DC +' PIN 36|-------|RST +' PIN 35|-------|BL +' GND |-------|GND +' 5V |-------|VIN +'-------- ------ + +' This example is based on the C library for Arduino: +' https://github.com/cbm80amiga/Arduino_ST7789_Fast/blob/master/Arduino_ST7789_Fast.cpp +' ------------------------------------------------------------------------------------- + +import ioio + +const MISO = 34 ' SPI MISO (unused) +const DIN = 40 ' SPI MOSI +const CLK = 39 ' SPI clock +const CS = 38 ' SPI Chip select +const DC = 37 ' Data or command -> HIGH = data / LOW = command +const RST = 36 ' Chip reset +const BL = 35 ' Blacklight control + +const ST7789_NOP = 0x00 +const ST7789_SWRESET = 0x01 +const ST7789_SLPOUT = 0x11 +const ST7789_NORON = 0x13 +const ST7789_INVON = 0x21 +const ST7789_DISPON = 0x29 +const ST7789_CASET = 0x2A +const ST7789_RASET = 0x2B +const ST7789_RAMWR = 0x2C +const ST7789_COLMOD = 0x3A +const ST7789_MADCTL = 0x36 +const ST7789_MADCTL_MY = 0x80 +const ST7789_MADCTL_MX = 0x40 +const ST7789_MADCTL_MV = 0x20 +const ST7789_MADCTL_ML = 0x10 +const ST7789_MADCTL_RGB = 0x00 +const ST7789_240x240_XSTART = 0 +const ST7789_240x240_YSTART = 0 +const ST7789_TFTWIDTH = 240 +const ST7789_TFTHEIGHT = 240 + +const BLACK = 0x0000 +const BLUE = 0x001F +const RED = 0xF800 +const GREEN = 0x07E0 +const CYAN = 0x07FF +const MAGENTA = 0xF81F +const YELLOW = 0xFFE0 +const WHITE = 0xFFFF + +const HIGH = TRUE +const LOW = FALSE +const PIN_DELAY = 1 + +colstart = 0 +rowstart = 0 +ystart = 0 +xstart = 0 +width = 240 +height = 240 + +Setup(240, 240) ' parameter: TFT width , TFT height +FillScreen(GREEN) + +'for xx = 100 to 150 +' DrawPixel(xx, 100, RGBto565(255, 0, 255)) +'next + +print "done" + +'######################################## + +sub Setup(w, h) + Print "Connect to TFT" + SPI = ioio.openSpiMaster(MISO, DIN, CLK, CS) + ResetPin = ioio.openDigitalOutput(RST) + DCPin = ioio.openDigitalOutput(DC) + BLPin = ioio.openDigitalOutput(BL) + ioio.waitForConnect(10) + Print "Connection established" + + if(w == 240 and h == 240) then rowstart = 80 + width = w + height = h + + ' Background light on + BLPin.write(HIGH) + + ' Hardware reset + ResetPin.write(HIGH) + delay(50) + ResetPin.write(LOW) + delay(50) + ResetPin.write(HIGH) + delay(150) + + 'Init + writeCmd(ST7789_SWRESET) : delay(150) + writeCmd(ST7789_SLPOUT) : delay(500) + writeCmd(ST7789_COLMOD) : writeData8(0x55) : delay(10) ' RGB565 + writeCmd(ST7789_MADCTL) : writeData8(0x00) + writeCmd(ST7789_CASET) : writeData16(ST7789_240x240_XSTART) : writeData16(ST7789_TFTWIDTH + ST7789_240x240_XSTART) + writeCmd(ST7789_RASET) : writeData16(ST7789_240x240_YSTART) : writeData16(ST7789_TFTHEIGHT + ST7789_240x240_YSTART) + writeCmd(ST7789_INVON) : delay(10) + writeCmd(ST7789_NORON) : delay(10) + writeCmd(ST7789_DISPON) : delay(10) + + SetRotation(2) +end + +func RGBto565(r,g,b) + return ((((r) BAND 0xF8) lshift 8) BOR (((g) BAND 0xFC) lshift 3) BOR ((b) rshift 3)) +end + +sub WriteCmd( c) + DCPin.write(LOW) + delay(PIN_DELAY) + SPI.write(c, ST7789_NOP) +end + +sub WriteData8(Data_Uint8) + DCPin.write(HIGH) + delay(PIN_DELAY) + SPI.write(Data_Uint8, 0) +end + +sub WriteData16(Data_Uint16) + DCPin.write(HIGH) + delay(PIN_DELAY) + SPI.write(Data_Uint16 rshift 8, Data_Uint16 BAND 0xFF) +end + +sub DrawPixel(x, y, c) + setAddrWindow(x, y, x + 1, y + 1) + WriteCmd(ST7789_RAMWR) + writeData16(c) +end + +sub FillRect(x, y, w, h, col) + if(x >= width OR y >= height OR w <= 0 OR h <= 0) then return + if(x + w - 1 >= width) then w = width - x + if(y + h - 1 >= height) then h = height - y + + setAddrWindow(x, y, x + w - 1, y + h - 1) + WriteCmd(ST7789_RAMWR) + + DCPin.write(HIGH) + delay(PIN_DELAY) + num = w * h + c_high = col rshift 8 + c_low = col BAND 0xFF + while(num) + num-- + SPI.write(c_high, c_low) + wend +end + +sub FillScreen(col) + FillRect(0, 0, width, height, col) +end + +sub SetRotation(m) + writeCmd(ST7789_MADCTL) + rotation = m BAND 3 + select case rotation + case 0 + writeData8(ST7789_MADCTL_MX BOR ST7789_MADCTL_MY BOR ST7789_MADCTL_RGB) + xstart = colstart + ystart = rowstart + case 1 + writeData8(ST7789_MADCTL_MY BOR ST7789_MADCTL_MV BOR ST7789_MADCTL_RGB) + ystart = colstart + xstart = rowstart + case 2 + writeData8(ST7789_MADCTL_RGB) + xstart = 0 + ystart = 0 + case 3 + writeData8(ST7789_MADCTL_MX BOR ST7789_MADCTL_MV BOR ST7789_MADCTL_RGB) + xstart = 0 + ystart = 0 + end select +end + + +sub setAddrWindow(xs, xe, ys, ye) + xs += xstart + xe += xstart + ys += ystart + ye += ystart + + 'CASET + WriteCmd(ST77XX_CASET) + DCPin.write(HIGH) ' data (active high) + SPI.write(xs rshift 8, xs BAND 0xFF) + SPI.write(xe rshift 8, xe BAND 0xFF) + ' RASET + WriteCmd(ST77XX_RASET) + DCPin.write(HIGH) ' data (active high) + SPI.write(ys rshift 8, ys BAND 0xFF) + SPI.write(ye rshift 8, ye BAND 0xFF) +end From 06d2ddd9c0e784115c787e4524eb4159f369008d Mon Sep 17 00:00:00 2001 From: chrisws Date: Wed, 29 May 2024 20:02:13 +0930 Subject: [PATCH 042/131] SpiMaster.write now takes an array argument #11 --- .../java/ioio/smallbasic/SpiMasterImpl.java | 9 ++-- .../java/ioio/smallbasic/TwiMasterImpl.java | 4 +- ioio/main.cpp | 6 +-- ioio/samples/duino-1088AS.bas | 50 ++++++++++++++++--- ioio/samples/veml6030.bas | 2 +- 5 files changed, 55 insertions(+), 16 deletions(-) diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/SpiMasterImpl.java b/ioio/ioio/src/main/java/ioio/smallbasic/SpiMasterImpl.java index cb87463..206c5f1 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/SpiMasterImpl.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/SpiMasterImpl.java @@ -1,6 +1,7 @@ package ioio.smallbasic; import java.io.IOException; +import java.util.Arrays; import ioio.lib.api.IOIO; import ioio.lib.api.SpiMaster; @@ -39,10 +40,12 @@ public void open(int miso, int mosi, int clk, int slaveSelect) throws IOExceptio validatePins(); } - public void write(int address, int data) { + public void write(int address, final byte[] write, int writeLen) { handleError(); lock.invoke((i) -> { - byte[] buffer = {(byte) address, (byte) data}; + byte[] buffer = new byte[writeLen + 1]; + buffer[0] = (byte)address; + System.arraycopy(write, 0, buffer, 1, writeLen); spiMaster.writeRead(buffer, buffer.length, buffer.length, null, 0); }); } @@ -54,7 +57,7 @@ void loop() throws ConnectionLostException, InterruptedException { @Override void setup(IOIO ioio) throws ConnectionLostException { - Log.i(TAG, "setup entered: " + miso + " " + mosi + " " + clk + " " + slaveSelect); + Log.i(TAG, "setup entered: miso:" + miso + " mosi:" + mosi + " clk:" + clk + " cs:" + slaveSelect); spiMaster = ioio.openSpiMaster(miso, mosi, clk, slaveSelect, SpiMaster.Rate.RATE_1M); } diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/TwiMasterImpl.java b/ioio/ioio/src/main/java/ioio/smallbasic/TwiMasterImpl.java index 8fe107a..fc0c58f 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/TwiMasterImpl.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/TwiMasterImpl.java @@ -48,10 +48,10 @@ public long readWrite(int address, int readLen, final byte[] write, int writeLen }); } - public void write(int address, final byte[] write, int len) { + public void write(int address, final byte[] write, int writeLen) { handleError(); lock.invoke((i) -> { - twiMaster.writeRead(address, false, write, len, null, 0); + twiMaster.writeRead(address, false, write, writeLen, null, 0); }); } diff --git a/ioio/main.cpp b/ioio/main.cpp index 55a6e89..b1ed1db 100644 --- a/ioio/main.cpp +++ b/ioio/main.cpp @@ -93,13 +93,11 @@ static int cmd_twimaster_write(var_s *self, int argc, slib_par_t *arg, var_s *re static int cmd_spimaster_write(var_s *self, int argc, slib_par_t *arg, var_s *retval) { int result = 0; if (argc != 2) { - error(retval, "SpiMaster.write", 2); + error(retval, "SpiMaster.write", ARRAY_SIZE); } else { int id = get_io_class_id(self, retval); if (id != -1) { - auto address = get_param_int(argc, arg, 0, 0); - auto data = get_param_int(argc, arg, 1, 0); - result = g_ioTaskMap.at(id).invokeVoidInt2("write", address, data, retval); + result = g_ioTaskMap.at(id).invokeWrite(argc, arg, retval); } } return result; diff --git a/ioio/samples/duino-1088AS.bas b/ioio/samples/duino-1088AS.bas index 87da026..60d5079 100644 --- a/ioio/samples/duino-1088AS.bas +++ b/ioio/samples/duino-1088AS.bas @@ -10,14 +10,52 @@ spi = ioio.openSpiMaster(misoPin, mosiPin, clkPin, csPin) ioio.waitForConnect(10) spi.write(0x09, 0x00) ' Decode mode: no decode for digits 0-7 -spi.write(0x0A, 0x01) ' Intensity: maximum intensity 0x0 -> 0x0F +spi.write(0x0A, 0x00) ' Intensity: maximum intensity 0x0 -> 0x0F spi.write(0x0B, 0x07) ' Scan limit: all digits spi.write(0x0C, 0x01) ' Shutdown: normal operation +for i = 1 to 8 + spi.write(i, 0) +next + +const glyph_a = [ + [0,0,0,0,1,0,0,0], + [0,0,0,1,0,1,0,0], + [0,0,1,0,0,0,1,0], + [0,0,1,0,0,0,1,0], + [0,0,1,1,1,1,1,0], + [0,1,0,0,0,0,0,1], + [0,1,0,0,0,0,0,1], + [0,0,0,0,0,0,0,0], +] + +const glyph_b = [ + [0,1,0,0,0,0,0,0], + [0,1,0,0,0,0,0,0], + [0,1,0,0,0,0,0,0], + [0,1,1,1,0,0,0,0], + [0,1,0,0,1,0,0,0], + [0,1,0,0,0,1,0,0], + [0,1,0,0,0,1,0,0], + [0,0,1,1,1,0,0,0], +] + +sub print_glyph(byref f) + local i, k, n + for i = 0 to 7 + n = 0 + for k = 0 to 7 + if (f[i][k] == 1) then + n += pow(2, 7 - k) + endif + next k + spi.write(i + 1, n) + next i +end + while 1 - for i = 1 to 8 - spi.write(i, 0x35) - next + print_glyph(glyph_a) + delay 1000 + print_glyph(glyph_b) delay 1000 -wend -' +wend diff --git a/ioio/samples/veml6030.bas b/ioio/samples/veml6030.bas index 2759a6f..2da76ab 100644 --- a/ioio/samples/veml6030.bas +++ b/ioio/samples/veml6030.bas @@ -34,7 +34,7 @@ p3 = ioio.openTwiMaster(1, 0) ioio.waitForConnect(10) rem configure default settings -p3.write(address, [alsConfReg, alsConf]) +p3.write(address, alsConfReg, alsConf) for i = 0 to 5 print p3.readWrite(address, 2, [alsDataReg]) From c96b8793c5ff758079db57555b53a96370b53e6a Mon Sep 17 00:00:00 2001 From: chrisws Date: Wed, 29 May 2024 20:43:30 +0930 Subject: [PATCH 043/131] Now errors when overflowing the fixed sized array buffer #11 --- include/javaproxy.h | 24 ++++++++++++++++-------- ioio/main.cpp | 2 +- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/include/javaproxy.h b/include/javaproxy.h index 9f08b51..ad221fe 100644 --- a/include/javaproxy.h +++ b/include/javaproxy.h @@ -16,7 +16,7 @@ JNIEnv *g_env; JavaVM *g_jvm; jobject g_activity; -#define ARRAY_SIZE 10 +#define ARRAY_SIZE 32 #if defined(ANDROID_MODULE) #define attachCurrentThread() g_jvm->AttachCurrentThread(&g_env, nullptr) @@ -288,15 +288,17 @@ struct JavaProxy { // int readWrite(int address, byte[] write) { int invokeReadWrite(int argc, slib_par_t *arg, var_s *retval) { int result = 0; - if (_instance != nullptr) { + int writeLen = populateByteArray(argc, arg, 2); + if (writeLen > ARRAY_SIZE) { + error(retval, "write array", 1, ARRAY_SIZE); + } else if (_instance != nullptr) { attachCurrentThread(); jmethodID method = g_env->GetMethodID(_clazz, "readWrite", "(II[BI)J"); var_int_t value = 0; if (method != nullptr) { auto address = get_param_int(argc, arg, 0, 0); auto readBytes = get_param_int(argc, arg, 1, 2); - populateByteArray(argc, arg, 2); - value = g_env->CallIntMethod(_instance, method, address, readBytes, _array, argc - 1); + value = g_env->CallIntMethod(_instance, method, address, readBytes, _array, writeLen); } if (!checkException(retval)) { v_setint(retval, value); @@ -310,13 +312,15 @@ struct JavaProxy { // int write(int address, byte[] write) { int invokeWrite(int argc, slib_par_t *arg, var_s *retval) { int result = 0; - if (_instance != nullptr) { + int writeLen = populateByteArray(argc, arg, 1); + if (writeLen > ARRAY_SIZE) { + error(retval, "write array", 1, ARRAY_SIZE); + } else if (_instance != nullptr) { attachCurrentThread(); jmethodID method = g_env->GetMethodID(_clazz, "write", "(I[BI)V"); if (method != nullptr) { auto address = get_param_int(argc, arg, 0, 0); - populateByteArray(argc, arg, 1); - g_env->CallVoidMethod(_instance, method, address, _array, argc - 1); + g_env->CallVoidMethod(_instance, method, address, _array, writeLen); } if (!checkException(retval)) { result = 1; @@ -327,7 +331,8 @@ struct JavaProxy { } // populate the java byte array with the contents of the basic array - void populateByteArray(int argc, slib_par_t *params, int offset) { + int populateByteArray(int argc, slib_par_t *params, int offset) { + int result; if (!_array) { _array = g_env->NewByteArray(ARRAY_SIZE); } @@ -340,13 +345,16 @@ struct JavaProxy { var_s *elem = v_elem(array, i); elements[i] = v_is_type(elem, V_INT) ? elem->v.i : elem->v.n; } + result = size; } else { for (int i = offset, j = 0; i < argc && i < ARRAY_SIZE; i++, j++) { elements[j] = get_param_int(argc, params, i, 0); } + result = argc - 1; } // make the changes available to the java side g_env->ReleaseByteArrayElements(_array, elements, 0); + return result; } protected: diff --git a/ioio/main.cpp b/ioio/main.cpp index b1ed1db..bbf2be4 100644 --- a/ioio/main.cpp +++ b/ioio/main.cpp @@ -93,7 +93,7 @@ static int cmd_twimaster_write(var_s *self, int argc, slib_par_t *arg, var_s *re static int cmd_spimaster_write(var_s *self, int argc, slib_par_t *arg, var_s *retval) { int result = 0; if (argc != 2) { - error(retval, "SpiMaster.write", ARRAY_SIZE); + error(retval, "SpiMaster.write", 2, ARRAY_SIZE); } else { int id = get_io_class_id(self, retval); if (id != -1) { From 19e89030558c3d740e80f3191a5f07a6b81c173a Mon Sep 17 00:00:00 2001 From: chrisws Date: Fri, 31 May 2024 21:24:20 +0930 Subject: [PATCH 044/131] Refactored the SPI write readWrite APIs --- include/javaproxy.h | 55 +------- ioio/README.md | 14 +-- ioio/api.json | 37 +++++- .../java/ioio/smallbasic/SpiMasterImpl.java | 20 ++- ioio/main.cpp | 117 +++++++++++++++++- ioio/mkapi.bas | 20 +-- ioio/mkdoc.bas | 10 +- .../{duino-1088AS.bas => duino-1088as.bas} | 0 8 files changed, 185 insertions(+), 88 deletions(-) rename ioio/samples/{duino-1088AS.bas => duino-1088as.bas} (100%) diff --git a/include/javaproxy.h b/include/javaproxy.h index ad221fe..3ccc222 100644 --- a/include/javaproxy.h +++ b/include/javaproxy.h @@ -285,51 +285,6 @@ struct JavaProxy { return result; } - // int readWrite(int address, byte[] write) { - int invokeReadWrite(int argc, slib_par_t *arg, var_s *retval) { - int result = 0; - int writeLen = populateByteArray(argc, arg, 2); - if (writeLen > ARRAY_SIZE) { - error(retval, "write array", 1, ARRAY_SIZE); - } else if (_instance != nullptr) { - attachCurrentThread(); - jmethodID method = g_env->GetMethodID(_clazz, "readWrite", "(II[BI)J"); - var_int_t value = 0; - if (method != nullptr) { - auto address = get_param_int(argc, arg, 0, 0); - auto readBytes = get_param_int(argc, arg, 1, 2); - value = g_env->CallIntMethod(_instance, method, address, readBytes, _array, writeLen); - } - if (!checkException(retval)) { - v_setint(retval, value); - result = 1; - } - detachCurrentThread(); - } - return result; - } - - // int write(int address, byte[] write) { - int invokeWrite(int argc, slib_par_t *arg, var_s *retval) { - int result = 0; - int writeLen = populateByteArray(argc, arg, 1); - if (writeLen > ARRAY_SIZE) { - error(retval, "write array", 1, ARRAY_SIZE); - } else if (_instance != nullptr) { - attachCurrentThread(); - jmethodID method = g_env->GetMethodID(_clazz, "write", "(I[BI)V"); - if (method != nullptr) { - auto address = get_param_int(argc, arg, 0, 0); - g_env->CallVoidMethod(_instance, method, address, _array, writeLen); - } - if (!checkException(retval)) { - result = 1; - } - detachCurrentThread(); - } - return result; - } - // populate the java byte array with the contents of the basic array int populateByteArray(int argc, slib_par_t *params, int offset) { int result; @@ -350,7 +305,7 @@ struct JavaProxy { for (int i = offset, j = 0; i < argc && i < ARRAY_SIZE; i++, j++) { elements[j] = get_param_int(argc, params, i, 0); } - result = argc - 1; + result = argc - offset; } // make the changes available to the java side g_env->ReleaseByteArrayElements(_array, elements, 0); @@ -382,7 +337,7 @@ extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void* reserved) { #else -int createJVM(const char *arg1, const char *arg2) { +int createJVM(const char *arg1, const char *arg2, bool debug) { JavaVMInitArgs vm_args; JavaVMOption options[6]; options[0].optionString = (char *)"-Xrs"; @@ -392,11 +347,7 @@ int createJVM(const char *arg1, const char *arg2) { options[4].optionString = (char *)"-agentlib:jdwp=transport=dt_socket,server=y,address=5005,suspend=y"; options[5].optionString = (char *)"-Xcheck:jni"; vm_args.version = JNI_VERSION_1_8; -#if defined(DEBUG) - vm_args.nOptions = 6; -#else - vm_args.nOptions = 3; -#endif + vm_args.nOptions = debug ? 6 : 3; vm_args.ignoreUnrecognized = 1; vm_args.options = options; int result = (JNI_CreateJavaVM(&g_jvm, (void **)&g_env, &vm_args) == JNI_OK && diff --git a/ioio/README.md b/ioio/README.md index 5f40b01..cc4463e 100644 --- a/ioio/README.md +++ b/ioio/README.md @@ -100,23 +100,23 @@ A pin used for PWM (Pulse-Width Modulation) output. A PWM pin produces a logic-l ## TwiMaster -An interface for controlling a TWI module, in TWI bus-master mode, enabling communication with multiple TWI-enabled slave modules. +An interface for controlling a TWI (Two Wire Interface) module, in TWI bus-master mode, enabling communication with multiple TWI-enabled slave modules. `io = ioio.openTwiMaster(TWINumber, mode)` -Opens a TWI module number TWINumber in master mode, using its dedicated SDA and SCL pins. The TWI module will run at 100KHz and will use I2C voltage levels if mode = false (pass true for SMBus levels). - | Name | Description | |---------|---------------| -|void write(Address, Register, DataByte) | Writes a byte of data to the given register of an I2C device with given address.| -|int readwrite(Address, NumReceiveBytes, Register, DataByte) | Writes a byte of data to the given register of an I2C device with given address and reads NumReceiveBytes. NumReceiveBytes can be max 8 bytes long.| +|void write(Address, Register, DataBytes)|Writes one or more bytes of data to the given register of an I2C device with given address.| +|int readWrite(address, NumReceiveBytes, Register, DataBytes)|Writes one or more bytes of data to the given register of an I2C device with given address and reads NumReceiveBytes. NumReceiveBytes can be max 8 bytes long.| ## SpiMaster -An interface for controlling an SPI module, in SPI bus-master mode, enabling communication with multiple SPI-enabled slave modules +An interface for controlling an SPI (Serial Peripheral Interface) module, in SPI bus-master mode, enabling communication with multiple SPI-enabled slave modules -`io = ioio.openSpiMaster(pin)` +`io = ioio.openSpiMaster(misoPin, mosiPin, clkPin, csPin)` | Name | Description | |---------|---------------| +|void write(DataBytes)|Writes one or more bytes of data.| +|int readWrite(NumReceiveBytes, DataBytes)|Writes one or more bytes of data and reads NumReceiveBytes. NumReceiveBytes can be max 8 bytes long.| diff --git a/ioio/api.json b/ioio/api.json index 4ba5b79..66b53e6 100644 --- a/ioio/api.json +++ b/ioio/api.json @@ -229,6 +229,7 @@ { "name": "PwmOutput", "comment": "A pin used for PWM (Pulse-Width Modulation) output. A PWM pin produces a logic-level PWM signal. These signals are typically used for simulating analog outputs for controlling the intensity of LEDs, the rotation speed of motors, etc. They are also frequently used for controlling hobby servo motors. PwmOutput instances are obtained by calling IOIO#openPwmOutput. When used for motors and LEDs, a frequency of several KHz is typically used, where there is a trade-off between switching power-loses and smoothness of operation. The pulse width is typically set by specifying the duty cycle, with the setDutyCycle method. A duty cycle of 0 is \"off\", a duty cycle of 1 is \"on\", and every intermediate value produces an intermediate intensity. Please note that any devices consuming more than 20mA of current (e.g. motors) should not by directly connected the the IOIO pins, but rather through an amplification circuit suited for the specific load. When used for hobby servos, the PWM signal is rather used for encoding of the desired angle the motor should go to. By standard, a 100Hz signal is used and the pulse width is varied between 1ms and 2ms (corresponding to both extremes of the shaft angle), using setPulseWidth. The instance is alive since its creation. If the connection with the IOIO drops at any point, the instance transitions to a disconnected state, in which every attempt to use the pin (except close()) will throw a ConnectionLostException. Whenever close() is invoked the instance may no longer be used. Any resources associated with it are freed and can be reused. Typical usage (fading LED):", + "signature": "pin, frequency", "pins": 2, "methods": [ { @@ -247,14 +248,38 @@ }, { "name": "TwiMaster": - "comment": "An interface for controlling a TWI module, in TWI bus-master mode, enabling communication with multiple TWI-enabled slave modules.", - "methods": [], - "pins": 2 + "comment": "An interface for controlling a TWI (Two Wire Interface) module, in TWI bus-master mode, enabling communication with multiple TWI-enabled slave modules.", + "signature": "TWINumber, mode", + "nogen" : true, + "pins": 2, + "methods": [{ + "name": "write", + "rtn": "void", + "signature": "Address, Register, DataBytes", + "comment": "Writes one or more bytes of data to the given register of an I2C device with given address." + },{ + "name": "readWrite", + "rtn": "int", + "signature": "address, NumReceiveBytes, Register, DataBytes", + "comment": "Writes one or more bytes of data to the given register of an I2C device with given address and reads NumReceiveBytes. NumReceiveBytes can be max 8 bytes long." + }] }, { "name": "SpiMaster": - "comment": "An interface for controlling an SPI module, in SPI bus-master mode, enabling communication with multiple SPI-enabled slave modules", - "methods": [], - "pins": 4 + "comment": "An interface for controlling an SPI (Serial Peripheral Interface) module, in SPI bus-master mode, enabling communication with multiple SPI-enabled slave modules", + "nogen" : true, + "signature": "misoPin, mosiPin, clkPin, csPin", + "pins": 4, + "methods": [{ + "name": "write", + "rtn": "void", + "signature": "DataBytes", + "comment": "Writes one or more bytes of data." + },{ + "name": "readWrite", + "rtn": "int", + "signature": "NumReceiveBytes, DataBytes", + "comment": "Writes one or more bytes of data and reads NumReceiveBytes. NumReceiveBytes can be max 8 bytes long." + }] } ] diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/SpiMasterImpl.java b/ioio/ioio/src/main/java/ioio/smallbasic/SpiMasterImpl.java index 206c5f1..c024c60 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/SpiMasterImpl.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/SpiMasterImpl.java @@ -40,13 +40,23 @@ public void open(int miso, int mosi, int clk, int slaveSelect) throws IOExceptio validatePins(); } - public void write(int address, final byte[] write, int writeLen) { + public long readWrite(int readLen, final byte[] write, int writeLen) { + handleError(); + return lock.invokeLong((i) -> { + byte[] read = new byte[readLen]; + spiMaster.writeRead(write, writeLen, writeLen, read, read.length); + long result = 0; + for (int index = 0; index < read.length; index++) { + result += ((long)Byte.toUnsignedInt(read[index])) << (index * 8); + } + return result; + }); + } + + public void write(final byte[] write, int writeLen) { handleError(); lock.invoke((i) -> { - byte[] buffer = new byte[writeLen + 1]; - buffer[0] = (byte)address; - System.arraycopy(write, 0, buffer, 1, writeLen); - spiMaster.writeRead(buffer, buffer.length, buffer.length, null, 0); + spiMaster.writeRead(write, write.length, write.length, null, 0); }); } diff --git a/ioio/main.cpp b/ioio/main.cpp index bbf2be4..38cacda 100644 --- a/ioio/main.cpp +++ b/ioio/main.cpp @@ -41,6 +41,94 @@ struct IOTask : JavaProxy { int open4(int pin1, int pin2, int pin3, int pin4, var_s *retval) { return invokeVoidInt4("open", pin1, pin2, pin3, pin4, retval); } + + // int readWrite(bytes, byte[] write) { + int invokeSpiReadWrite(int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + int writeLen = populateByteArray(argc, arg, 1); + if (writeLen > ARRAY_SIZE) { + error(retval, "write array", 1, ARRAY_SIZE); + } else if (_instance != nullptr) { + attachCurrentThread(); + jmethodID method = g_env->GetMethodID(_clazz, "readWrite", "(I[BI)J"); + var_int_t value = 0; + if (method != nullptr) { + auto readBytes = get_param_int(argc, arg, 0, 2); + value = g_env->CallIntMethod(_instance, method, readBytes, _array, writeLen); + } + if (!checkException(retval)) { + v_setint(retval, value); + result = 1; + } + detachCurrentThread(); + } + return result; + } + + // int write(byte[] write) { + int invokeSpiWrite(int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + int writeLen = populateByteArray(argc, arg, 0); + if (writeLen > ARRAY_SIZE) { + error(retval, "write array", 1, ARRAY_SIZE); + } else if (_instance != nullptr) { + attachCurrentThread(); + jmethodID method = g_env->GetMethodID(_clazz, "write", "([BI)V"); + if (method != nullptr) { + g_env->CallVoidMethod(_instance, method, _array, writeLen); + } + if (!checkException(retval)) { + result = 1; + } + detachCurrentThread(); + } + return result; + } + + // int readWrite(int address, byte[] write) { + int invokeTwiReadWrite(int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + int writeLen = populateByteArray(argc, arg, 2); + if (writeLen > ARRAY_SIZE) { + error(retval, "write array", 1, ARRAY_SIZE); + } else if (_instance != nullptr) { + attachCurrentThread(); + jmethodID method = g_env->GetMethodID(_clazz, "readWrite", "(II[BI)J"); + var_int_t value = 0; + if (method != nullptr) { + auto address = get_param_int(argc, arg, 0, 0); + auto readBytes = get_param_int(argc, arg, 1, 2); + value = g_env->CallIntMethod(_instance, method, address, readBytes, _array, writeLen); + } + if (!checkException(retval)) { + v_setint(retval, value); + result = 1; + } + detachCurrentThread(); + } + return result; + } + + // int write(int address, byte[] write) { + int invokeTwiWrite(int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + int writeLen = populateByteArray(argc, arg, 1); + if (writeLen > ARRAY_SIZE) { + error(retval, "write array", 1, ARRAY_SIZE); + } else if (_instance != nullptr) { + attachCurrentThread(); + jmethodID method = g_env->GetMethodID(_clazz, "write", "(I[BI)V"); + if (method != nullptr) { + auto address = get_param_int(argc, arg, 0, 0); + g_env->CallVoidMethod(_instance, method, address, _array, writeLen); + } + if (!checkException(retval)) { + result = 1; + } + detachCurrentThread(); + } + return result; + } }; robin_hood::unordered_map g_ioTaskMap; @@ -71,7 +159,7 @@ static int cmd_twimaster_readwrite(var_s *self, int argc, slib_par_t *arg, var_s } else { int id = get_io_class_id(self, retval); if (id != -1) { - result = g_ioTaskMap.at(id).invokeReadWrite(argc, arg, retval); + result = g_ioTaskMap.at(id).invokeTwiReadWrite(argc, arg, retval); } } return result; @@ -84,7 +172,23 @@ static int cmd_twimaster_write(var_s *self, int argc, slib_par_t *arg, var_s *re } else { int id = get_io_class_id(self, retval); if (id != -1) { - result = g_ioTaskMap.at(id).invokeWrite(argc, arg, retval); + result = g_ioTaskMap.at(id).invokeTwiWrite(argc, arg, retval); + } + } + return result; +} + +static int cmd_spimaster_readwrite(var_s *self, int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + auto readBytes = get_param_int(argc, arg, 0, 0); + if (argc < 2) { + error(retval, "SpiMaster.readWrite(read-bytes, [data]", 2, ARRAY_SIZE); + } else if (readBytes < 1 || readBytes > 8) { + error(retval, "read-bytes value out of range. Expected a number between 1 and 8"); + } else { + int id = get_io_class_id(self, retval); + if (id != -1) { + result = g_ioTaskMap.at(id).invokeSpiReadWrite(argc, arg, retval); } } return result; @@ -92,12 +196,12 @@ static int cmd_twimaster_write(var_s *self, int argc, slib_par_t *arg, var_s *re static int cmd_spimaster_write(var_s *self, int argc, slib_par_t *arg, var_s *retval) { int result = 0; - if (argc != 2) { - error(retval, "SpiMaster.write", 2, ARRAY_SIZE); + if (argc < 1) { + error(retval, "SpiMaster.write", 1, ARRAY_SIZE); } else { int id = get_io_class_id(self, retval); if (id != -1) { - result = g_ioTaskMap.at(id).invokeWrite(argc, arg, retval); + result = g_ioTaskMap.at(id).invokeSpiWrite(argc, arg, retval); } } return result; @@ -110,6 +214,7 @@ static void create_twimaster(var_t *map) { static void create_spimaster(var_t *map) { v_create_callback(map, "write", cmd_spimaster_write); + v_create_callback(map, "readWrite", cmd_spimaster_readwrite); } #include "api.h" @@ -149,7 +254,7 @@ SBLIB_API int sblib_func_count() { // int sblib_init(const char *sourceFile) { #if defined(DESKTOP_MODULE) - int result = createJVM("-Djava.class.path=./ioio-1.0-jar-with-dependencies.jar", "-Dioio.SerialPorts=IOIO0"); + int result = createJVM("-Djava.class.path=./ioio-1.0-jar-with-dependencies.jar", "-Dioio.SerialPorts=IOIO0", true); #else int result = 1; #endif diff --git a/ioio/mkapi.bas b/ioio/mkapi.bas index b5c86aa..351009e 100644 --- a/ioio/mkapi.bas +++ b/ioio/mkapi.bas @@ -54,17 +54,17 @@ sub generate_command(objName, method) print " if (argc != " + param_count + ") {" print " error(retval, \"" + err_name + "\", " + param_count + ");" print " } else {" - + local getter, indent if (objName == "IOIO") then getter = "g_ioioTask->" indent = " " - else + else getter = "g_ioTaskMap.at(id)." indent = " " print " int id = get_io_class_id(self, retval);" print " if (id != -1) {" - endif + endif local argument = "" if (method.arg == "boolean" || method.arg == "int") then @@ -112,7 +112,7 @@ sub generate_open_function(byref obj) for pin = 2 to obj.pins openFunc += "pin" + pin + ", " print " int pin" + pin + " = get_param_int(argc, params, " + (pin - 1) + ", -1);" - next + next endif print " int id = ++g_nextId;" print " IOTask &instance = g_ioTaskMap[id];" @@ -131,13 +131,15 @@ sub generate_open_function(byref obj) end for obj in api - for method in obj.methods - generate_command(obj.name, method) - next + if (obj.nogen == 0) then + for method in obj.methods + generate_command(obj.name, method) + next + endif next for obj in api - if (len(obj.methods) > 0 && obj.name != "IOIO") then + if (len(obj.methods) > 0 && obj.name != "IOIO" and obj.nogen == 0) then generate_constructor(obj) endif next @@ -145,6 +147,6 @@ next for obj in api if (obj.name != "IOIO") then generate_open_function(obj) - endif + endif next diff --git a/ioio/mkdoc.bas b/ioio/mkdoc.bas index 972fcd9..863c970 100644 --- a/ioio/mkdoc.bas +++ b/ioio/mkdoc.bas @@ -18,7 +18,9 @@ func get_signature(method) result += " " + method.name + "(" - if (method.arg == "void") then + if (method.signature != 0) then + result += method.signature + else if (method.arg == "void") then result += "void" else if (method.arg == "boolean" || method.arg == "int") then result += "int" @@ -42,8 +44,10 @@ for obj in api print obj.comment if (obj.name != "IOIO") then print - print "`io = ioio.open" + obj.name + "(pin)`" - endif + signature = "(pin)" + if (obj.signature) then signature = "(" + obj.signature + ")" + print "`io = ioio.open" + obj.name + signature + "`" + endif print "" print "| Name | Description |" print "|---------|---------------|" diff --git a/ioio/samples/duino-1088AS.bas b/ioio/samples/duino-1088as.bas similarity index 100% rename from ioio/samples/duino-1088AS.bas rename to ioio/samples/duino-1088as.bas From 2117d01f81ce0950d5eff78362d1e42436f09c7b Mon Sep 17 00:00:00 2001 From: chrisws Date: Fri, 31 May 2024 21:26:32 +0930 Subject: [PATCH 045/131] Bump up size of the JNI data transfer array --- include/javaproxy.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/javaproxy.h b/include/javaproxy.h index 3ccc222..2ef49a4 100644 --- a/include/javaproxy.h +++ b/include/javaproxy.h @@ -16,7 +16,7 @@ JNIEnv *g_env; JavaVM *g_jvm; jobject g_activity; -#define ARRAY_SIZE 32 +#define ARRAY_SIZE 1024 #if defined(ANDROID_MODULE) #define attachCurrentThread() g_jvm->AttachCurrentThread(&g_env, nullptr) From f4b7d256b19a4bbfc53f6f8faa9216a6d8d718f8 Mon Sep 17 00:00:00 2001 From: chrisws Date: Sat, 1 Jun 2024 14:33:32 +0930 Subject: [PATCH 046/131] fix spi-master regression --- include/javaproxy.h | 3 ++- ioio/ioio/src/main/java/ioio/smallbasic/SpiMasterImpl.java | 4 ++-- ioio/main.cpp | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/include/javaproxy.h b/include/javaproxy.h index 2ef49a4..29237c3 100644 --- a/include/javaproxy.h +++ b/include/javaproxy.h @@ -16,7 +16,8 @@ JNIEnv *g_env; JavaVM *g_jvm; jobject g_activity; -#define ARRAY_SIZE 1024 +// TWI limit is 255, SPI limit is 64 +#define ARRAY_SIZE 255 #if defined(ANDROID_MODULE) #define attachCurrentThread() g_jvm->AttachCurrentThread(&g_env, nullptr) diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/SpiMasterImpl.java b/ioio/ioio/src/main/java/ioio/smallbasic/SpiMasterImpl.java index c024c60..be1dfa0 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/SpiMasterImpl.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/SpiMasterImpl.java @@ -56,7 +56,7 @@ public long readWrite(int readLen, final byte[] write, int writeLen) { public void write(final byte[] write, int writeLen) { handleError(); lock.invoke((i) -> { - spiMaster.writeRead(write, write.length, write.length, null, 0); + spiMaster.writeRead(write, writeLen, writeLen, null, 0); }); } @@ -90,7 +90,7 @@ private void validatePins() { mosi == clk || mosi == slaveSelect || clk == slaveSelect) { - IOUtil.setError("One or pins have duplicate values"); + IOUtil.setError("One or more pins have duplicate values"); } } } diff --git a/ioio/main.cpp b/ioio/main.cpp index 38cacda..2522040 100644 --- a/ioio/main.cpp +++ b/ioio/main.cpp @@ -254,7 +254,7 @@ SBLIB_API int sblib_func_count() { // int sblib_init(const char *sourceFile) { #if defined(DESKTOP_MODULE) - int result = createJVM("-Djava.class.path=./ioio-1.0-jar-with-dependencies.jar", "-Dioio.SerialPorts=IOIO0", true); + int result = createJVM("-Djava.class.path=./ioio-1.0-jar-with-dependencies.jar", "-Dioio.SerialPorts=IOIO0", false); #else int result = 1; #endif From 711f708763ba0f7b252d9a88a997d5c2f73cdef4 Mon Sep 17 00:00:00 2001 From: chrisws Date: Sat, 1 Jun 2024 23:04:12 +0930 Subject: [PATCH 047/131] st7735s work in progress --- ioio/samples/st7735s.bas | 157 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 ioio/samples/st7735s.bas diff --git a/ioio/samples/st7735s.bas b/ioio/samples/st7735s.bas new file mode 100644 index 0000000..fc14618 --- /dev/null +++ b/ioio/samples/st7735s.bas @@ -0,0 +1,157 @@ +rem +rem Educational BoosterPack MKII - Color TFT LCD Display +rem +rem The Crystalfontz CFAF128128B-0145T color 128x128-pixel TFT LCD supports display updates up to +rem 20 frames per second (FPS) while only requiring a few lines to control the TFT LCD module through +rem the SPI interface. This module has a color depth of 262K colors and a contrast ratio of 350. +rem +rem https://www.ti.com/tool/BOOSTXL-EDUMKII +rem https://www.ti.com/document-viewer/lit/html/SLAU599B#GUID-3E8385B7-69DD-4133-9660-C9C256762AA8/TITLE-SLAU599SLAU5992619 +rem https://www.crystalfontz.com/product/cfaf128128b0145t-graphical-tft-128x128-lcd-display-module +rem https://www.crystalfontz.com/controllers/Sitronix/ST7735S/ +rem https://github.com/crystalfontz/CFAF128128B1-0145T +rem https://github.com/crystalfontz/CFAF128128B1-0145T/blob/master/CFAF128128B1-0145T_SPI_Demo_Code/CFAF128128B1-0145T_SPI_Demo_Code.ino +rem + +import ioio + +const ST7735_SLPOUT 0x11 +const ST7735_DISPON 0x29 +const ST7735_CASET 0x2A +const ST7735_RASET 0x2B +const ST7735_RAMWR 0x2C +const ST7735_RAMRD 0x2E +const ST7735_MADCTL 0x36 +const ST7735_COLMOD 0x3A +const ST7735_FRMCTR1 0xB1 +const ST7735_FRMCTR2 0xB2 +const ST7735_FRMCTR3 0xB3 +const ST7735_INVCTR 0xB4 +const ST7735_PWCTR1 0xC0 +const ST7735_PWCTR2 0xC1 +const ST7735_PWCTR3 0xC2 +const ST7735_PWCTR4 0xC3 +const ST7735_PWCTR5 0xC4 +const ST7735_VMCTR1 0xC5 +const ST7735_GAMCTRP1 0xE0 +const ST7735_GAMCTRN1 0xE1 + +rem J2.15 - LCD SPI MOSI +const mosiPin = 2 + +rem Unused - no data received from lcd +const misoPin = 6 + +rem J2.13 - LCD SPI chip select CS +rem Low: Controller chip is selected. Communications with host is possible. +rem High: Controller chip is not selected. Host interface signals are ignored by the controller. +const csPin = 3 + +' J1.7 - LCD SPI clock +const clkPin = 4 + +' J4.31 - LCD register select pin +const rsPin = 5 + +rem J4.17 - LCD reset pin +rem Low: Display controller is reset. The RST pin should be pulsed low shortly after power is applied. +rem High: The RST pin should be brought high (VDD) for normal operation. +const rstPin = 7 + +rem J4.39 - LCD backlight (Pin is multiplexed with the RGB LED red channel pin through the jumper header J5) +rem not implemented + +const spi = ioio.openSpiMaster(misoPin, mosiPin, clkPin, csPin) +const rsOut = ioio.openDigitalOutput(rsPin) +const rstOut = ioio.openDigitalOutput(rstPin) + +ioio.waitForConnect(10) + +sub sendCommand(cmd) + ' Select the LCD's command register + rsOut.write(0) + spi.write(cmd) +end + +sub sendData(_data) + rsOut.write(1) + spi.write(_data) +end + +sub resetDisplay() + rstOut.write(0) + delay 50 + rstOut.write(1) + delay 50 +end + +sub initST7735S() + resetDisplay() + sendCommand(0x01) ' Software reset + delay 150 + sendCommand(ST7735_SLPOUT) + delay 255 + sendCommand(ST7735_DISPON) +end + +sub set_LCD_for_write_at_X_Y(x, y) + rem CASET (2Ah): Column Address Set + rem * The value of XS [15:0] and XE [15:0] are referred when RAMWR + rem command comes. + rem * Each value represents one column line in the Frame Memory. + rem * XS [15:0] always must be equal to or less than XE [15:0] + sendCommand(ST7735_CASET) ' Column address set + rem Write the parameters for the "column address set" command + 'sendData(0x00) ' Start MSB = XS[15:8] + 'sendData(0x02 + x) ' Start LSB = XS[ 7:0] + 'sendData(0x00) ' End MSB = XE[15:8] + 'sendData(0x81) ' End LSB = XE[ 7:0] + local _data = [0x00, 0x02 + x, 0x00, 0x81] + sendData(_data) + + rem Write the "row address set" command to the LCD + rem RASET (2Bh): Row Address Set + rem * The value of YS [15:0] and YE [15:0] are referred when RAMWR + rem command comes. + rem * Each value represents one row line in the Frame Memory. + rem * YS [15:0] always must be equal to or less than YE [15:0] + sendCommand(ST7735_RASET) ' Row address set + + rem Write the parameters for the "row address set" command + 'sendData(0x00) ' Start MSB = YS[15:8] + 'sendData(0x01 + y) ' Start LSB = YS[ 7:0] + 'sendData(0x00) ' End MSB = YE[15:8] + 'sendData(0x80) ' End LSB = YE[ 7:0] + _data = [0x00, 0x01 + y, 0x00, 0x80] + sendData(_data) + + rem Write the "write data" command to the LCD + rem RAMWR (2Ch): Memory Write + sendCommand(ST7735_RAMWR) ' write data +end + +rem Fill display with a given RGB value +sub fill_LCD(r, g, b) + local i, _data + Set_LCD_for_write_at_X_Y(0, 0) + + for i = 1 to 21 + _data << b + _data << g + _data << r + next + + rsOut.write(1) + for i = 0 to (128 * 128) / 21 + spi.write(_data) + next i +end + +rem/Write the single pixel's worth of data +sub put_Pixel(x, y, r, g, b) + Set_LCD_for_write_at_X_Y(x, y) + sendData([b,g,r]) +end + +initST7735S() +fill_LCD(1, 212, 31) From 1d8fcb743369668ba3915a081acafbd1195c854c Mon Sep 17 00:00:00 2001 From: Joerg Siebenmorgen Date: Sat, 1 Jun 2024 21:27:46 +0200 Subject: [PATCH 048/131] Add SSD1306 --- ioio/samples/canvas.bas | 238 ++++++++++++++++++++++++++++++++++++++ ioio/samples/font7x4.json | 1 + ioio/samples/ssd1306.bas | 177 ++++++++++++++++++++++++++++ 3 files changed, 416 insertions(+) create mode 100644 ioio/samples/canvas.bas create mode 100644 ioio/samples/font7x4.json create mode 100644 ioio/samples/ssd1306.bas diff --git a/ioio/samples/canvas.bas b/ioio/samples/canvas.bas new file mode 100644 index 0000000..2691a78 --- /dev/null +++ b/ioio/samples/canvas.bas @@ -0,0 +1,238 @@ +unit canvas + +export create +export draw_line +export draw_pixel +export draw_char +export draw_circle +export draw_rect +export draw_rect_filled +export draw_string + +const font = load_font("font7x4.json") + +sub iterate(w, h, filter) + local x, y + for y = 0 to h - 1 + for x = 0 to w - 1 + call filter, x, y + next + next +end + +func load_font(name) + local font = {} + local col = 0 + local row = 0 + local ch, txt, buffer + local w = 4 + local h = 7 + + tload name, txt, 1 + buffer = array(txt) + + func slice + local result + dim result(h, w) + sub do_slice(x, y) + result[y, x] = buffer[y + row, x + col] + end + iterate(w, h, @do_slice) + return result + end + + for ch = asc("a") to asc("j") + c = chr(ch) + font[c] = slice() + col += w + 1 + next + + row += h + 1 + col = 0 + for ch = asc("k") to asc("t") + c = chr(ch) + font[c] = slice() + col += w + 1 + next + + row += h + 1 + col = 0 + for ch = asc("u") to asc("z") + c = chr(ch) + font[c] = slice() + col += w + 1 + next + + row += h + 1 + row += h + 1 + col = 0 + for ch = asc("0") to asc("9") + c = chr(ch) + font[c] = slice() + col += w + 1 + next + + col = 30 + row = 16 + font[" "] = slice() + + col += w + 1 + font["."] = slice() + + col += w + 1 + font[","] = slice() + + col += w + 1 + font["!"] = slice() + + return font +end + +sub draw_pixel(byref canvas, x1, y1) + if (x1 > -1 and y1 > -1 and y1 <= ubound(canvas._dat, 1) and x1 <= ubound(canvas._dat, 2)) then + canvas._dat[y1, x1] = canvas._pen + endif +end + +func draw_char(byref canvas, c, x0, y0) + local glyph = font[lower(c)] + local g_height = ubound(glyph, 1) + local g_width = ubound(glyph, 2) + local c_height = canvas._fontSize + local c_width = int(c_height * .55) + local yscale = c_height / g_height + local xscale = c_width / g_height + + sub filter(x, y) + local x1 = round(x / xscale) + local y1 = round(y / yscale) + if (y0 + y <= canvas._h and x0 + x <= canvas._w and y1 <= g_height and x1 <= g_width and glyph[y1, x1] != 0) then + canvas._dat[y0 + y, x0 + x] = canvas._pen + endif + end + + sub transfer(x, y) + if (glyph[y, x] != 0) then + canvas._dat[y0 + y, x0 + x] = canvas._pen + endif + end + + if (canvas._fontSize < 19) then + iterate(g_width, g_height, @transfer) + else + iterate(c_width, c_height, @filter) + endif + + return [g_height, g_width] +end + +sub draw_string(byref canvas, s, x0, y0) + local x = x0 + local y = y0 + local c, m, spacing + for c in s + m = draw_char(canvas, c, x, y) + spacing = m[1] / m[0] * canvas._fontSize * .35 + x += m[1] + spacing + next +end + +sub draw_line(byref canvas, x0, y0, x1, y1) + local dx = abs(x1 - x0) + local dy = -abs(y1 - y0) + local sx = iff(x0 < x1, 1, -1) + local sy = iff(y0 < y1, 1, -1) + local err = dx + dy + local e2 + + while 1 + draw_pixel(canvas, x0, y0) + if (x0 == x1 and y0 == y1) then exit loop + if ((sx < 0 and x0 < 0) or (sy < 0 and y0 < 0)) then exit loop + e2 = err * 2 + if (e2 >= dy) then + err += dy + x0 += sx + endif + if (e2 <= dx) then + err += dx + y0 += sy + endif + wend +end + +sub draw_rect(byref canvas, x0, y0, x1, y1) + draw_line(canvas, x0, y0, x1, y0) + draw_line(canvas, x0, y1, x1, y1) + draw_line(canvas, x0, y0, x0, y1) + draw_line(canvas, x1, y0, x1, y1) +end + +sub draw_rect_filled(byref canvas, x0, y0, x1, y1) + local x, y + for y = max(y0, 0) to min(y1, canvas._h) + for x = max(x0, 0) to min(x1, canvas._w) + canvas._dat[y, x] = canvas._pen + next + next +end + +sub draw_circle(byref canvas, x0, y0, r, as_filled) + sub fill_circle(x, y, r) + draw_line canvas, -x + x0, y + y0, x + x0, y + y0 + draw_line canvas, -x + x0, -y + y0, x + x0, -y + y0 + draw_line canvas, -y + x0, x + y0, y + x0, x + y0 + draw_line canvas, -y + x0, -x + y0, y + x0, -x + y0 + end + + sub plot_circle(x, y, r) + ' left + right mid quads + draw_pixel canvas, x + x0, y + y0 + draw_pixel canvas, -x + x0, y + y0 + draw_pixel canvas, x + x0, -y + y0 + draw_pixel canvas, -x + x0, -y + y0 + ' left + right top/bottom quads + draw_pixel canvas, y + x0, x + y0 + draw_pixel canvas, -y + x0, x + y0 + draw_pixel canvas, y + x0, -x + y0 + draw_pixel canvas, -y + x0, -x + y0 + end + + local painter + if (as_filled) then + painter = @fill_circle + else + painter = @plot_circle + endif + + local x = r + local y = 0 + local p = 1 - r + + call painter, x, y, r + while (x > y) + y++ + if (p <= 0) then + ' Mid-point is inside or on the perimeter + p += (2 * y) + 1 + else + ' Mid-point is outside the perimeter + x-- + p += (2 * y) - (2 * x) + 1 + endif + + ' All the perimeter points have already been printed + if (x < y) then exit loop + call painter, x, y, r + wend +end + +func create(width, height, _pen) + local result = {} + dim result._dat(height, width) + result._pen = _pen + result._fontSize = 20 + result._w = width + result._h = height + return result +end diff --git a/ioio/samples/font7x4.json b/ioio/samples/font7x4.json new file mode 100644 index 0000000..e8d4f5b --- /dev/null +++ b/ioio/samples/font7x4.json @@ -0,0 +1 @@ +[1,1,1,1,0,1,1,1,0,0,0,1,1,1,0,1,1,1,0,0,1,1,1,1,0,1,1,1,1,0,1,1,1,1,0,1,0,0,1,0,1,1,1,0,0,0,0,0,1,1;1,0,0,1,0,1,0,0,1,0,1,0,0,0,0,1,0,0,1,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,1,0,0,1,0,0,0,0,0,0,1,1;1,0,0,1,0,1,0,0,1,0,1,0,0,0,0,1,0,0,1,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,1,0,0,1,0,0,0,0,0,0,1,1;1,1,1,1,0,1,1,1,1,0,1,0,0,0,0,1,0,0,1,0,1,1,1,0,0,1,1,1,0,0,1,0,1,1,0,1,1,1,1,0,0,1,0,0,0,0,0,0,1,1;1,0,0,1,0,1,0,0,1,0,1,0,0,0,0,1,0,0,1,0,1,0,0,0,0,1,0,0,0,0,1,0,0,1,0,1,0,0,1,0,0,1,0,0,0,0,0,0,1,1;1,0,0,1,0,1,0,0,1,0,1,0,0,0,0,1,0,0,1,0,1,0,0,0,0,1,0,0,0,0,1,0,0,1,0,1,0,0,1,0,0,1,0,0,0,0,0,0,1,1;1,0,0,1,0,1,1,1,0,0,0,1,1,1,0,1,1,1,0,0,1,1,1,1,0,1,0,0,0,0,1,1,1,1,0,1,0,0,1,0,1,1,1,0,0,1,1,1,0,1;0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1;1,0,0,1,0,1,0,0,0,0,1,0,0,1,0,1,0,0,1,0,0,1,1,0,0,1,1,1,0,0,0,1,1,0,0,1,1,1,0,0,0,1,1,1,0,1,1,1,1,1;1,0,0,1,0,1,0,0,0,0,1,1,1,1,0,1,0,0,1,0,1,0,0,1,0,1,0,0,1,0,1,0,0,1,0,1,0,0,1,0,1,0,0,0,0,0,0,1,0,1;1,0,0,1,0,1,0,0,0,0,1,1,1,1,0,1,1,0,1,0,1,0,0,1,0,1,0,0,1,0,1,0,0,1,0,1,0,0,1,0,1,0,0,0,0,0,0,1,0,1;1,1,1,0,0,1,0,0,0,0,1,0,0,1,0,1,1,0,1,0,1,0,0,1,0,1,1,1,0,0,1,0,0,1,0,1,1,1,0,0,0,1,1,0,0,0,0,1,0,1;1,0,0,1,0,1,0,0,0,0,1,0,0,1,0,1,0,1,1,0,1,0,0,1,0,1,0,0,0,0,1,0,0,1,0,1,0,0,1,0,0,0,0,1,0,0,0,1,0,1;1,0,0,1,0,1,0,0,0,0,1,0,0,1,0,1,0,1,1,0,1,0,0,1,0,1,0,0,0,0,1,0,1,1,0,1,0,0,1,0,0,0,0,1,0,0,0,1,0,1;1,0,0,1,0,1,1,1,1,0,1,0,0,1,0,1,0,0,1,0,0,1,1,0,0,1,0,0,0,0,0,1,0,1,0,1,0,0,1,0,1,1,1,0,0,0,0,1,0,1;0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1;1,0,0,1,0,1,0,0,1,0,1,0,0,1,0,1,0,0,1,0,1,0,0,1,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1;1,0,0,1,0,1,0,0,1,0,1,0,0,1,0,1,0,0,1,0,1,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1;1,0,0,1,0,0,1,0,1,0,1,0,0,1,0,1,0,0,1,0,1,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1;1,0,0,1,0,0,1,0,1,0,1,0,0,1,0,0,1,1,0,0,0,1,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1;1,0,0,1,0,0,0,1,1,0,1,1,1,1,0,1,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1;1,0,0,1,0,0,0,1,1,0,1,1,1,1,0,1,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1;0,1,1,0,0,0,0,0,1,0,1,0,0,1,0,1,0,0,1,0,0,0,1,0,0,1,1,1,1,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,1;0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1;1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,1,0,0,0,0,0,0,1,0,0,0,1,1,1,0,1,0,0,1,0,1,0,0,1,0,1,1,1,1,1;0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,1,0,0,0,0,1,1,1,1,0,1,0,0,0,0,1,0,0,1,0,1,0,0,1,0,0,0,0,0,1;0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,1;1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1;0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,1,0,0,1,0,0,1,0,0,1,1,0,0,0,1,1,0,1;0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,1,0,0,0,0,1,0,0,1,0,1,0,0,1,0,0,0,0,0,1;0,1,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,1,1,0,0,1,1,0,0,1,0,0,1,0,1,1,1,1,1;0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1;0,1,1,0,0,0,0,1,0,0,0,1,1,0,0,0,1,1,0,0,0,0,1,1,0,1,1,1,1,0,0,1,1,0,0,1,1,1,1,0,0,1,1,0,0,0,1,1,0,1;1,0,0,1,0,0,1,1,0,0,1,0,0,1,0,1,0,0,1,0,0,1,0,1,0,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,1,0,0,1,0,1,0,0,1,1;1,0,1,1,0,1,0,1,0,0,1,0,1,0,0,0,0,0,1,0,1,0,0,1,0,1,0,1,0,0,1,0,0,0,0,0,0,1,0,0,1,0,0,1,0,1,0,0,1,1;1,0,1,1,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,1,1,1,1,0,1,1,0,1,0,1,1,1,0,0,0,0,1,0,0,0,1,1,0,0,0,1,1,1,1;1,1,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,1,0,0,1,0,0,1,0,0,0,1,0,0,1,0,0,0,0,1,1;1,1,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,1,0,0,0,0,1,0,1,0,0,1,0,1,0,0,1,0,0,1,0,0,0,1,0,0,1,0,1,0,0,1,1;0,1,1,0,0,0,1,1,1,0,1,1,1,1,0,0,1,1,0,0,0,0,0,1,0,0,1,1,0,0,0,1,1,0,0,1,0,0,0,0,0,1,1,0,0,0,1,1,0,1;1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] diff --git a/ioio/samples/ssd1306.bas b/ioio/samples/ssd1306.bas new file mode 100644 index 0000000..5f4c24c --- /dev/null +++ b/ioio/samples/ssd1306.bas @@ -0,0 +1,177 @@ +' SSD1306 - I2C OLED TFT with 128x64 monochrome pixels +' =============================================== +' +' This example demonstrates how to draw graphics on a +' SSD1306 OLED display connected via I2C +' +' Connect the display to the IOIO-OTG board: +' +' ------ ------ +' IOIO | |SSD1306 +' PIN 4|-------|SDA +' PIN 5|-------|SCL +' GND |-------|GND +' 3.3V |-------|VCC +'------- ------ + +import ioio ' module for IOIO Board +import canvas ' unit to draw graphics in a framebuffer + +const ADDRESS = 0x3C +const WIDTH = 128 +const HEIGHT = 64 + +Print "Connect to SSD1306 OLED display" +oled = ioio.openTwiMaster(0, 0) +ioio.waitForConnect(10) +Print "Connection established" + +Init_ssd1306() + +' Init canvas +c = canvas.create(WIDTH, HEIGHT, 1) +c._fontSize = 7 + +' Draw some graphics +c._pen = 0 ' Set draw color to 0 +canvas.draw_rect_filled(c, 0,0,WIDTH,HEIGHT) ' Clear screen +c._pen = 1 ' Set draw color to 1 +canvas.draw_circle(c, 25, 40, 16, true) +canvas.draw_string(c, "IOIO with", 60, 0) +canvas.draw_string(c, "SMALLBASIC", 60, 15) +canvas.draw_line(c, 0, 0, 127, 63) +canvas.draw_line(c, 0, 0, 0, 63) +canvas.draw_line(c, 0, 63, 127, 63) + +' Transfer framebuffer to display +TransferFramebuffer(c._dat) ' c._dat is the canvas framebuffer +delay(1000) + +' Set brightness +SetBrightness(20) +delay(1000) +SetBrightness(255) +delay(1000) + +' Display on/off +DisplayOff() +delay(1000) +DisplayOn() +delay(1000) + +' Set inverse +SetInverse(TRUE) +delay(1000) +SetInverse(FALSE) + +print "done" + +'############################################## + +sub SendCommand(c) + oled.write(ADDRESS, 0, c) +end + +sub Init_ssd1306() + ' Init sequence according to specsheet + ' Display off + SendCommand(0xAE) + ' Set Multiplex Ratio + SendCommand(0xA8) + SendCommand(SSD1306_LCDHEIGHT - 1) + ' Display Offset + SendCommand(0xD3) + SendCommand(0x0) + ' Set Display Clock Divide Ratio / Oscillator Frequency; suggested ratio 0x80 + SendCommand(0xD5) + SendCommand(0x80) + ' Display Start Line -> 0 + SendCommand(0x40) + ' Set Segment Re-map: column address 127 is mapped to SEG0 + SendCommand(0xA1) + ' Set COM Output Scan Direction: remapped mode. Scan from COM[N-1] to COM0 + SendCommand(0xC8) + ' Set COM Pins Hardware Configuration + ' Alternative COM pin configuration + Disable COM Left/Right remap + SendCommand(0xDA) + SendCommand(0x12) + ' Set Contrast Control (Brightness) + ' 0 to 255 + SendCommand(0x81) + SendCommand(200) + ' Entire Display ON: A4h command enable display outputs according to the GDDRAM contents + SendCommand(0xA4) + ' Set Normal Display: This command sets the display to be either normal or inverse. + ' In normal display a RAM data of 1 indicates an β€œON” pixel while in inverse display + ' a RAM data of 0 indicates an β€œON” pixel + ' 0xA6 normal, 0xA7 inverse + SendCommand(0xA6) ' <- for testing set to inverse. The display should be white. + ' Charge Pump Setting + ' Enable Charge Pump + SendCommand(0x8D) + SendCommand(0x14) + ' Memory Addressing Mode + ' Horizontal Addressing Mode + SendCommand(0x20) + SendCommand(0x00) + ' turn on display + SendCommand(0xAF) +end + +sub TransferFramebuffer(byref fb) + local ii, xx, yy, aa, chunk, t, FRAMEBUFFER_SIZE, FrameBuffer_1bit + + FRAMEBUFFER_SIZE = WIDTH * HEIGHT / 8 + dim FrameBuffer_1bit(FRAMEBUFFER_SIZE) + + ' The display is monochrome. A on-pixel is 1, a off-pixel is 0. + ' In display RAM every byte stores 8 pixels. The canvas framebuffer + ' needs to be rearranged. + ii = 0 + for xx = 0 to HEIGHT - 8 Step 8 + for yy = 0 to WIDTH - 1 + FrameBuffer_1bit[ii] = (fb[xx+7,yy] lshift 7) BOR (fb[xx+6,yy] lshift 6) BOR (fb[xx+5,yy] lshift 5) BOR (fb[xx+4,yy] lshift 4) BOR (fb[xx+3,yy] lshift 3) BOR (fb[xx+2,yy] lshift 2) BOR (fb[xx+1,yy] lshift 1) BOR fb[xx,yy] + ii++ + next + next + + ' The framebuffer can't be send in one go (restriction by IOIO). + ' The framebuffer is cut in chunks each 33 bytes long. Every + ' chunk is transfered sequentially. The first element of every + ' chunk is the command 0x40 for writing to display RAM. The + ' following 32 bytes are framebuffer data. + dim chunk[32] + + for ii = 0 to FRAMEBUFFER_SIZE - 32 step 32 + for aa = 1 to 32 + chunk[0] = 0x40 + chunk[aa] = FrameBuffer_1bit[ii + aa - 1] + next + oled.write(ADDRESS, chunk) + next +end + +sub SetBrightness(b) + ' Set Contrast Control (Brightness) + ' 0 to 255 + if(b < 0) then b = 0 + if(b > 255) then b = 255 + SendCommand(0x81) + SendCommand(b) +end + +sub DisplayOn() + SendCommand(0xAF) +end + +sub DisplayOff() + SendCommand(0xAE) +end + +sub SetInverse(i) + if(i) then + SendCommand(0xA7) + else + SendCommand(0xA6) + endif +end From 7b669d7b084b2c693b0dbc2c2a8d4cb68454d727 Mon Sep 17 00:00:00 2001 From: chrisws Date: Sun, 2 Jun 2024 10:52:30 +0930 Subject: [PATCH 049/131] Implemented SPI batched write for slight performance improvement --- include/javaproxy.h | 3 +-- .../java/ioio/smallbasic/SpiMasterImpl.java | 19 +++++++++++++++---- ioio/main.cpp | 14 ++++++++------ ioio/samples/st7735s.bas | 17 ++++++++++++----- 4 files changed, 36 insertions(+), 17 deletions(-) diff --git a/include/javaproxy.h b/include/javaproxy.h index 29237c3..d6386a5 100644 --- a/include/javaproxy.h +++ b/include/javaproxy.h @@ -16,8 +16,7 @@ JNIEnv *g_env; JavaVM *g_jvm; jobject g_activity; -// TWI limit is 255, SPI limit is 64 -#define ARRAY_SIZE 255 +#define ARRAY_SIZE 254 #if defined(ANDROID_MODULE) #define attachCurrentThread() g_jvm->AttachCurrentThread(&g_env, nullptr) diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/SpiMasterImpl.java b/ioio/ioio/src/main/java/ioio/smallbasic/SpiMasterImpl.java index be1dfa0..5350b3d 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/SpiMasterImpl.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/SpiMasterImpl.java @@ -1,15 +1,16 @@ package ioio.smallbasic; -import java.io.IOException; -import java.util.Arrays; - import ioio.lib.api.IOIO; import ioio.lib.api.SpiMaster; import ioio.lib.api.exception.ConnectionLostException; import ioio.lib.spi.Log; +import java.io.IOException; + public class SpiMasterImpl extends IOTask { private static final String TAG = "SpiMasterImpl"; + private static final int SPI_WRITE_MAX = 63; + private final byte[] BATCH = new byte[SPI_WRITE_MAX]; private final IOLock lock = new IOLock<>(); private SpiMaster spiMaster = null; private int miso; @@ -56,7 +57,17 @@ public long readWrite(int readLen, final byte[] write, int writeLen) { public void write(final byte[] write, int writeLen) { handleError(); lock.invoke((i) -> { - spiMaster.writeRead(write, writeLen, writeLen, null, 0); + if (writeLen < SPI_WRITE_MAX) { + spiMaster.writeRead(write, writeLen, writeLen, null, 0); + } else { + int srcPos = 0; + while (srcPos < writeLen) { + int batchLen = Math.min(writeLen - srcPos, SPI_WRITE_MAX); + System.arraycopy(write, srcPos, BATCH, 0, batchLen); + spiMaster.writeRead(BATCH, batchLen, batchLen, null, 0); + srcPos += SPI_WRITE_MAX; + } + } }); } diff --git a/ioio/main.cpp b/ioio/main.cpp index 2522040..fb934dd 100644 --- a/ioio/main.cpp +++ b/ioio/main.cpp @@ -25,6 +25,8 @@ #define CLASS_SPIMASTER "ioio/smallbasic/SpiMasterImpl" #define CLASS_IOIO "ioio/smallbasic/IOIOImpl" #define CLASS_IOTASK_ID 1 +#define SPI_WRITE_MAX 63 +#define TWI_WRITE_MAX ARRAY_SIZE struct IOTask : JavaProxy { IOTask() : JavaProxy() { @@ -46,8 +48,8 @@ struct IOTask : JavaProxy { int invokeSpiReadWrite(int argc, slib_par_t *arg, var_s *retval) { int result = 0; int writeLen = populateByteArray(argc, arg, 1); - if (writeLen > ARRAY_SIZE) { - error(retval, "write array", 1, ARRAY_SIZE); + if (writeLen > SPI_WRITE_MAX) { + error(retval, "write array", 1, SPI_WRITE_MAX); } else if (_instance != nullptr) { attachCurrentThread(); jmethodID method = g_env->GetMethodID(_clazz, "readWrite", "(I[BI)J"); @@ -89,8 +91,8 @@ struct IOTask : JavaProxy { int invokeTwiReadWrite(int argc, slib_par_t *arg, var_s *retval) { int result = 0; int writeLen = populateByteArray(argc, arg, 2); - if (writeLen > ARRAY_SIZE) { - error(retval, "write array", 1, ARRAY_SIZE); + if (writeLen > TWI_WRITE_MAX) { + error(retval, "write array", 1, TWI_WRITE_MAX); } else if (_instance != nullptr) { attachCurrentThread(); jmethodID method = g_env->GetMethodID(_clazz, "readWrite", "(II[BI)J"); @@ -113,8 +115,8 @@ struct IOTask : JavaProxy { int invokeTwiWrite(int argc, slib_par_t *arg, var_s *retval) { int result = 0; int writeLen = populateByteArray(argc, arg, 1); - if (writeLen > ARRAY_SIZE) { - error(retval, "write array", 1, ARRAY_SIZE); + if (writeLen > TWI_WRITE_MAX) { + error(retval, "write array", 1, TWI_WRITE_MAX); } else if (_instance != nullptr) { attachCurrentThread(); jmethodID method = g_env->GetMethodID(_clazz, "write", "(I[BI)V"); diff --git a/ioio/samples/st7735s.bas b/ioio/samples/st7735s.bas index fc14618..3498891 100644 --- a/ioio/samples/st7735s.bas +++ b/ioio/samples/st7735s.bas @@ -16,6 +16,7 @@ rem import ioio const ST7735_SLPOUT 0x11 +const ST7735_DISPOFF 0x28 const ST7735_DISPON 0x29 const ST7735_CASET 0x2A const ST7735_RASET 0x2B @@ -82,7 +83,7 @@ sub resetDisplay() rstOut.write(0) delay 50 rstOut.write(1) - delay 50 + delay 150 end sub initST7735S() @@ -133,16 +134,18 @@ end rem Fill display with a given RGB value sub fill_LCD(r, g, b) local i, _data - Set_LCD_for_write_at_X_Y(0, 0) + ' higher values didn't improve performance + local bufSize = 40 - for i = 1 to 21 + for i = 1 to bufSize _data << b _data << g _data << r next + Set_LCD_for_write_at_X_Y(0, 0) rsOut.write(1) - for i = 0 to (128 * 128) / 21 + for i = 0 to (128 * 128) / bufSize spi.write(_data) next i end @@ -154,4 +157,8 @@ sub put_Pixel(x, y, r, g, b) end initST7735S() -fill_LCD(1, 212, 31) +randomize +t1 = timer +fill_LCD(rnd*255, rnd*255, rnd*255) +print format("Elap: ###", timer-t1) +delay 5000 From ab62b6bb3141b22fe61e20a096cb3703a2d8837f Mon Sep 17 00:00:00 2001 From: Joerg Siebenmorgen Date: Fri, 7 Jun 2024 22:14:25 +0200 Subject: [PATCH 050/131] Add MPU6050 example --- ioio/samples/mpu6050.bas | 114 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 ioio/samples/mpu6050.bas diff --git a/ioio/samples/mpu6050.bas b/ioio/samples/mpu6050.bas new file mode 100644 index 0000000..bdf26a1 --- /dev/null +++ b/ioio/samples/mpu6050.bas @@ -0,0 +1,114 @@ +' MPU6050 - Accelerometer, gyroscope and temperature sensor +' ========================================================= +' +' This examample demonstrates how to use +' the MPU-6050 I2C sensor. +' +' Connect the sensor to the IOIO-OTG board: +' +' ------ ------ +' IOIO | |MPU6050 +' PIN 4|-------|SDA +' PIN 5|-------|SCL +' GND |-------|GND +' 3.3V |-------|VIN +' | |XCL +' | |XDA +' | |ADD +' | |INT +'------- ------ + +' If ADD is open or connected to GND, 0x68 as I2C address +' will be used. Otherwise 0x69. +' Don't connect XCL, XDA, ADD and INT +' +' "https://github.com/tockn/MPU6050_tockn" was very helpful +' to get the sensor working + +import ioio + +const ADDRESS = 0x68 + +Print "Connect to MPU-6050" +sensor = ioio.openTwiMaster(0, 0) +ioio.waitForConnect(10) +Print "Connection established" + +delay(500) + +WhoamI = sensor.readwrite(ADDRESS, 1, 0x75) +print "WHO_AM_I: ", hex(WhoamI) ' Check for connection: sensor returns 0x68 + +' SMPLRT_DIV +sensor.write(ADDRESS, 0x19, 0x00) +' MPU config +sensor.write(ADDRESS, 0x1A, 0x00) +' Gyro config +sensor.write(ADDRESS, 0x1B, 0x08) +' Accel config +sensor.write(ADDRESS, 0x1C, 0x00) +' Turn on +sensor.write(ADDRESS, 0x6B, 0x01) + +for ii = 1 to 1000 + + A = GetAcceleration() + G = GetGyroscope() + T = GetTemperature() + + locate 5,0 + print "Acc: ["; + print USING "##.00 "; A.AccX, A.AccY, A.AccZ; + print "] Gryo: ["; + print USING "####.00 "; G.GyrX, G.GyrY, G.GyrZ; + print "] Temp : "; + print USING "##.00 "; T + + delay(100) + showpage +next + + +func GetAcceleration() + local A + dim A + + A.AccX = short((sensor.readwrite(ADDRESS, 1, 0x3B) lshift 8) BOR sensor.readwrite(ADDRESS, 1, 0x3C)) / 16384 + A.AccY = short((sensor.readwrite(ADDRESS, 1, 0x3D) lshift 8) BOR sensor.readwrite(ADDRESS, 1, 0x3E)) / 16384 + A.AccZ = short((sensor.readwrite(ADDRESS, 1, 0x3F) lshift 8) BOR sensor.readwrite(ADDRESS, 1, 0x40)) / 16384 + + return A +end + +func GetGyroscope() + local d + dim d + + d.GyrX = short((sensor.readwrite(ADDRESS, 1, 0x43) lshift 8) BOR sensor.readwrite(ADDRESS, 1, 0x44)) / 65.5 + d.GyrY = short((sensor.readwrite(ADDRESS, 1, 0x45) lshift 8) BOR sensor.readwrite(ADDRESS, 1, 0x46)) / 65.5 + d.GyrZ = short((sensor.readwrite(ADDRESS, 1, 0x47) lshift 8) BOR sensor.readwrite(ADDRESS, 1, 0x48)) / 65.5 + + return d +end + +func GetTemperature() + return short((sensor.readwrite(ADDRESS, 1, 0x41) lshift 8) BOR sensor.readwrite(ADDRESS, 1, 0x42)) / 340 + 36.53 +end + +func CalculateAccelerationAngle(AccX, AccY, AccZ) + local d + dim d + + d.AngleAccX = atan2(AccY, sqr(AccZ^2 + AccX^2)) * 360 / 2.0 / PI + d.AngleAccY = atan2(AccX, sqr(AccZ^2 * AccY^2)) * 360 / -2.0 / PI + + return d +end + +func short(dat) + if dat > 32767 then + return dat - 65536 + else + return dat + endif +end From 0fb6a3278b0047bf9daec98c8d7feda171f1ad73 Mon Sep 17 00:00:00 2001 From: chrisws Date: Sun, 16 Jun 2024 19:20:03 +0930 Subject: [PATCH 051/131] Update dependencies --- gtk-server/uthash | 2 +- nuklear/Nuklear | 2 +- raylib/README.md | 8 ++++++-- raylib/func-def.h | 4 ++++ raylib/func.h | 43 +++++++++++++++++++++++++++++++++++++++++++ raylib/mkraylib.bas | 2 ++ raylib/proc.h | 8 ++++---- raylib/raygui | 2 +- raylib/raylib | 2 +- websocket/mongoose | 2 +- 10 files changed, 64 insertions(+), 11 deletions(-) diff --git a/gtk-server/uthash b/gtk-server/uthash index eeba196..619fe95 160000 --- a/gtk-server/uthash +++ b/gtk-server/uthash @@ -1 +1 @@ -Subproject commit eeba1961f203869116a865e57c968e9c86e1b8c4 +Subproject commit 619fe95ca4679249528f086536aadd0c469dbd99 diff --git a/nuklear/Nuklear b/nuklear/Nuklear index 31b5729..504f831 160000 --- a/nuklear/Nuklear +++ b/nuklear/Nuklear @@ -1 +1 @@ -Subproject commit 31b5729d66403e4ca1b6ff6df2958c49f3bc2c8d +Subproject commit 504f8319a8211a24b49f68330b7a8962d8e4632c diff --git a/raylib/README.md b/raylib/README.md index 2d24b43..e4ffd64 100644 --- a/raylib/README.md +++ b/raylib/README.md @@ -4,7 +4,7 @@ raylib is a simple and easy-to-use library to enjoy videogames programming. https://www.raylib.com/ -Implemented APIs (613) +Implemented APIs (617) ---------------- | Name | Description | @@ -19,6 +19,7 @@ Implemented APIs (613) | func ChangeDirectory(dir) | Change working directory, return true on success | | func CheckCollisionBoxes(box1, box2) | Check collision between two bounding boxes | | func CheckCollisionBoxSphere(box, center, radius) | Check collision between box and sphere | +| func CheckCollisionCircleLine(center, radius, p1, p2) | Check if circle collides with a line created betweeen two points [p1] and [p2] | | func CheckCollisionCircleRec(center, radius, rec) | Check collision between circle and rectangle | | func CheckCollisionCircles(center1, radius1, center2, radius2) | Check collision between two circles | | func CheckCollisionLines(startPos1, endPos1, startPos2, endPos2, collisionPoint) | Check the collision between two lines defined by two points each, returns collision point by reference | @@ -381,6 +382,7 @@ Implemented APIs (613) | func IsCursorOnScreen() | Check if cursor is on the screen | | func IsFileDropped() | Check if a file has been dropped into window | | func IsFileExtension(fileName, ext) | Check file extension (including point: .png, .wav) | +| func IsFileNameValid(fileName) | Check if fileName is valid for the platform/OS | | func IsFontReady(font) | Check if a font is ready | | func IsGamepadAvailable(gamepad) | Check if a gamepad is available | | func IsGamepadButtonDown(gamepad, button) | Check if a gamepad button is being pressed | @@ -574,10 +576,12 @@ Implemented APIs (613) | func TextLength(text) | Get text length, checks for '\\0' ending | | func TextReplace(text, replace, by) | Replace text string (WARNING: memory must be freed!) | | func TextSubtext(text, position, length) | Get a piece of a text string | +| func TextToCamel(text) | Get Camel case notation version of provided string | | func TextToFloat(text) | Get float value from text (negative values not supported) | | func TextToInteger(text) | Get integer value from text (negative values not supported) | | func TextToLower(text) | Get lower case version of provided string | | func TextToPascal(text) | Get Pascal case notation version of provided string | +| func TextToSnake(text) | Get Snake case notation version of provided string | | func TextToUpper(text) | Get upper case version of provided string | | sub ToggleBorderlessWindowed() | Toggle window state: borderless windowed (only PLATFORM_DESKTOP) | | sub ToggleFullscreen() | Toggle window state: fullscreen/windowed (only PLATFORM_DESKTOP) | @@ -619,7 +623,7 @@ Implemented APIs (613) | func waitevents() | n/a | | sub WaitTime(seconds) | Wait for some time (halt program execution) | | func WaveCopy(wave) | Copy a wave to a new wave | -| sub WaveCrop(wave, initSample, finalSample) | Crop a wave to defined samples range | +| sub WaveCrop(wave, initFrame, finalFrame) | Crop a wave to defined frames range | | sub WaveFormat(wave, sampleRate, sampleSize, channels) | Convert wave data to desired format | | func WindowShouldClose() | Check if application should close (KEY_ESCAPE pressed or windows close icon clicked) | diff --git a/raylib/func-def.h b/raylib/func-def.h index 44fe99b..bcc7597 100644 --- a/raylib/func-def.h +++ b/raylib/func-def.h @@ -1,6 +1,7 @@ {1, 1, "CHANGEDIRECTORY", cmd_changedirectory}, {2, 2, "CHECKCOLLISIONBOXES", cmd_checkcollisionboxes}, {3, 3, "CHECKCOLLISIONBOXSPHERE", cmd_checkcollisionboxsphere}, + {4, 4, "CHECKCOLLISIONCIRCLELINE", cmd_checkcollisioncircleline}, {3, 3, "CHECKCOLLISIONCIRCLEREC", cmd_checkcollisioncirclerec}, {4, 4, "CHECKCOLLISIONCIRCLES", cmd_checkcollisioncircles}, {5, 5, "CHECKCOLLISIONLINES", cmd_checkcollisionlines}, @@ -164,6 +165,7 @@ {0, 0, "ISCURSORONSCREEN", cmd_iscursoronscreen}, {0, 0, "ISFILEDROPPED", cmd_isfiledropped}, {2, 2, "ISFILEEXTENSION", cmd_isfileextension}, + {1, 1, "ISFILENAMEVALID", cmd_isfilenamevalid}, {1, 1, "ISFONTREADY", cmd_isfontready}, {1, 1, "ISGAMEPADAVAILABLE", cmd_isgamepadavailable}, {2, 2, "ISGAMEPADBUTTONDOWN", cmd_isgamepadbuttondown}, @@ -252,10 +254,12 @@ {1, 1, "TEXTLENGTH", cmd_textlength}, {3, 3, "TEXTREPLACE", cmd_textreplace}, {3, 3, "TEXTSUBTEXT", cmd_textsubtext}, + {1, 1, "TEXTTOCAMEL", cmd_texttocamel}, {1, 1, "TEXTTOFLOAT", cmd_texttofloat}, {1, 1, "TEXTTOINTEGER", cmd_texttointeger}, {1, 1, "TEXTTOLOWER", cmd_texttolower}, {1, 1, "TEXTTOPASCAL", cmd_texttopascal}, + {1, 1, "TEXTTOSNAKE", cmd_texttosnake}, {1, 1, "TEXTTOUPPER", cmd_texttoupper}, {1, 1, "WAVECOPY", cmd_wavecopy}, {0, 0, "WINDOWSHOULDCLOSE", cmd_windowshouldclose}, diff --git a/raylib/func.h b/raylib/func.h index 0432151..cd252fe 100644 --- a/raylib/func.h +++ b/raylib/func.h @@ -31,6 +31,19 @@ static int cmd_checkcollisionboxsphere(int argc, slib_par_t *params, var_t *retv return 1; } +// +// Check if circle collides with a line created betweeen two points [p1] and [p2] +// +static int cmd_checkcollisioncircleline(int argc, slib_par_t *params, var_t *retval) { + auto center = get_param_vec2(argc, params, 0); + auto radius = get_param_num(argc, params, 1, 0); + auto p1 = get_param_vec2(argc, params, 2); + auto p2 = get_param_vec2(argc, params, 3); + auto fnResult = CheckCollisionCircleLine(center, radius, p1, p2); + v_setint(retval, fnResult); + return 1; +} + // // Check collision between circle and rectangle // @@ -1944,6 +1957,16 @@ static int cmd_isfileextension(int argc, slib_par_t *params, var_t *retval) { return 1; } +// +// Check if fileName is valid for the platform/OS +// +static int cmd_isfilenamevalid(int argc, slib_par_t *params, var_t *retval) { + auto fileName = get_param_str(argc, params, 0, 0); + auto fnResult = IsFileNameValid(fileName); + v_setint(retval, fnResult); + return 1; +} + // // Check if a font is ready // @@ -3010,6 +3033,16 @@ static int cmd_textsubtext(int argc, slib_par_t *params, var_t *retval) { return 1; } +// +// Get Camel case notation version of provided string +// +static int cmd_texttocamel(int argc, slib_par_t *params, var_t *retval) { + auto text = get_param_str(argc, params, 0, 0); + auto fnResult = (const char *)TextToCamel(text); + v_setstr(retval, fnResult); + return 1; +} + // // Get float value from text (negative values not supported) // @@ -3050,6 +3083,16 @@ static int cmd_texttopascal(int argc, slib_par_t *params, var_t *retval) { return 1; } +// +// Get Snake case notation version of provided string +// +static int cmd_texttosnake(int argc, slib_par_t *params, var_t *retval) { + auto text = get_param_str(argc, params, 0, 0); + auto fnResult = (const char *)TextToSnake(text); + v_setstr(retval, fnResult); + return 1; +} + // // Get upper case version of provided string // diff --git a/raylib/mkraylib.bas b/raylib/mkraylib.bas index 0e6f612..ee64008 100644 --- a/raylib/mkraylib.bas +++ b/raylib/mkraylib.bas @@ -62,6 +62,8 @@ func get_param_name(byref param) case "modelanimation *": result = "(ModelAnimation *)get_param_int_t" case "modelanimation*": result = "(ModelAnimation *)get_param_int_t" case "texture2d *": result = "(Texture2D *)get_param_int_t" + case "const vector2 *": result = "(Vector2 *)get_param_vec2_array" + case "const vector3 *": result = "(Vector3 *)get_param_vec3_array" case "vector2 *": result = "(Vector2 *)get_param_vec2_array" case "vector3 *": result = "(Vector3 *)get_param_vec3_array" case "wave *": result = "(Wave *)get_param_int_t" diff --git a/raylib/proc.h b/raylib/proc.h index 45d7227..52aa170 100644 --- a/raylib/proc.h +++ b/raylib/proc.h @@ -3426,13 +3426,13 @@ static int cmd_waittime(int argc, slib_par_t *params, var_t *retval) { } // -// Crop a wave to defined samples range +// Crop a wave to defined frames range // static int cmd_wavecrop(int argc, slib_par_t *params, var_t *retval) { auto wave = (Wave *)get_param_int_t(argc, params, 0, 0); - auto initSample = get_param_int(argc, params, 1, 0); - auto finalSample = get_param_int(argc, params, 2, 0); - WaveCrop(wave, initSample, finalSample); + auto initFrame = get_param_int(argc, params, 1, 0); + auto finalFrame = get_param_int(argc, params, 2, 0); + WaveCrop(wave, initFrame, finalFrame); return 1; } diff --git a/raylib/raygui b/raylib/raygui index 6f53233..4b3d94f 160000 --- a/raylib/raygui +++ b/raylib/raygui @@ -1 +1 @@ -Subproject commit 6f532337ff2d6dc14ba3a5f5b7ee63f9d90ff13b +Subproject commit 4b3d94f5df6a5a2aa86286350f7e20c0ca35f516 diff --git a/raylib/raylib b/raylib/raylib index e0f6faa..b4fbdc0 160000 --- a/raylib/raylib +++ b/raylib/raylib @@ -1 +1 @@ -Subproject commit e0f6faa151589a185a04c2c723c01daff1b0a78f +Subproject commit b4fbdc028302f9a697f196e8d02a7dca28912f59 diff --git a/websocket/mongoose b/websocket/mongoose index 59020e0..5adbadc 160000 --- a/websocket/mongoose +++ b/websocket/mongoose @@ -1 +1 @@ -Subproject commit 59020e0d5db10b453fb688daaf515bae6e9f582d +Subproject commit 5adbadc9010a6c8416374b7ee3bc2789428a3e4e From eee25656e797e1d25d33dd43ab28626ad1dbdf7c Mon Sep 17 00:00:00 2001 From: Joerg Siebenmorgen Date: Fri, 5 Jul 2024 22:28:34 +0200 Subject: [PATCH 052/131] Add HD44780 example --- ioio/samples/hd44780.bas | 225 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 ioio/samples/hd44780.bas diff --git a/ioio/samples/hd44780.bas b/ioio/samples/hd44780.bas new file mode 100644 index 0000000..a1791f7 --- /dev/null +++ b/ioio/samples/hd44780.bas @@ -0,0 +1,225 @@ +' HD44780 - Text LCD +' ================== +' +' This example demonstrates how to use the alphanumeric +' dot matrix liquid crystal display HD44780. +' +' Connect the LCD to the IOIO-OTG board: +' +' ------- ------ 1 (GND) +' IOIO | |HD44780 | +' PIN 41|-------|11 (DB4) --- +' PIN 42|-------|12 (DB5) | | +' PIN 43|-------|13 (DB6) |10K|<---3 (VEE) +' PIN 44|-------|14 (DB7) | | +' GND |-------| 5 (RW) --- +' PIN 45|-------| 6 (E) | +' PIN 46|-------| 4 (RS) 2 (VIN) +' GND |-------| 1 (GND) +' 5V |-------| 2 (VIN) +'-------- -------- + +' A potentiometer needs to be connected to the display +' to control the contrast of the display. If the display +' has a background light, connect power according to the +' data sheet to the pins 15 and 16. +' +' Code based on https://www.mikrocontroller.net/articles/AVR-GCC-Tutorial/LCD-Ansteuerung + + +'######################################################### +'# Constant definition # +'######################################################### + +const LCD_CLEAR_DISPLAY = 0x01 +const LCD_CURSOR_HOME = 0x02 + +const LCD_SET_ENTRY = 0x04 +const LCD_ENTRY_DECREASE = 0x00 +const LCD_ENTRY_INCREASE = 0x02 +const LCD_ENTRY_NOSHIFT = 0x00 +const LCD_ENTRY_SHIFT = 0x01 + +const LCD_SET_DISPLAY = 0x08 +const LCD_DISPLAY_OFF = 0x00 +const LCD_DISPLAY_ON = 0x04 +const LCD_CURSOR_OFF = 0x00 +const LCD_CURSOR_ON = 0x02 +const LCD_BLINKING_OFF = 0x00 +const LCD_BLINKING_ON = 0x01 + +const LCD_SET_SHIFT = 0x10 +const LCD_CURSOR_MOVE = 0x00 +const LCD_DISPLAY_SHIFT = 0x08 +const LCD_SHIFT_LEFT = 0x00 +const LCD_SHIFT_RIGHT = 0x04 + +const LCD_SET_FUNCTION = 0x20 +const LCD_FUNCTION_4BIT = 0x00 +const LCD_FUNCTION_8BIT = 0x10 +const LCD_FUNCTION_1LINE = 0x00 +const LCD_FUNCTION_2LINE = 0x08 +const LCD_FUNCTION_5X7 = 0x00 +const LCD_FUNCTION_5X10 = 0x04 + +const LCD_SOFT_RESET = 0x30 + +const LCD_SET_CGADR = 0x40 + +const LCD_DDADR_LINE1 = 0x00 +const LCD_DDADR_LINE2 = 0x40 +const LCD_DDADR_LINE3 = 0x10 +const LCD_DDADR_LINE4 = 0x50 + +const LCD_GC_CHAR0 = 0 +const LCD_GC_CHAR1 = 1 +const LCD_GC_CHAR2 = 2 +const LCD_GC_CHAR3 = 3 +const LCD_GC_CHAR4 = 4 +const LCD_GC_CHAR5 = 5 +const LCD_GC_CHAR6 = 6 +const LCD_GC_CHAR7 = 7 + +const LCD_SET_DDADR = 0x80 + +'######################################################### +'# Main program # +'######################################################### + +import ioio + +Print "Connect to HD44780" +RS = ioio.openDigitalOutput(46) +E = ioio.openDigitalOutput(45) +DB4 = ioio.openDigitalOutput(41) +DB5 = ioio.openDigitalOutput(42) +DB6 = ioio.openDigitalOutput(43) +DB7 = ioio.openDigitalOutput(44) +ioio.waitForConnect(10) +Print "Connection established" + + +Init() +LCD_Write(" Hello World") +LCD_Locate(1,2) +LCD_Write("-= SmallBASIC =-") +print "Done" + + +end + +'######################################################### +'# Functions and subs # +'######################################################### + +sub Init() + + RS.write(0) + E.write(0) + DB4.write(0) + DB5.write(0) + DB6.write(0) + DB7.write(0) + + delay(50) + + ' Send soft-reset 3 time to initialize LCD + Send4Bit(LCD_SOFT_RESET) + delay(5) + SendEnable() + delay(1) + SendEnable() + delay(1) + + ' Set 4-bit mode + Send4Bit(LCD_SET_FUNCTION BOR LCD_FUNCTION_4BIT) + delay(5) + + ' 2 lines and 5x7 pixel in 4 bit mode + SendCommand(LCD_SET_FUNCTION BOR LCD_FUNCTION_4BIT BOR LCD_FUNCTION_2LINE BOR LCD_FUNCTION_5X7) + ' Display on, cursor off and blinking off + SendCommand(LCD_SET_DISPLAY BOR LCD_DISPLAY_ON BOR LCD_CURSOR_OFF BOR LCD_BLINKING_OFF) + ' Cursor increment no scrolling + SendCommand(LCD_SET_ENTRY BOR LCD_ENTRY_INCREASE BOR LCD_ENTRY_NOSHIFT ) + + LCD_Cls() +end + +sub LCD_Write(Text) + ' Write characters to lcd + local length, t + length = len(Text) + + for i = 1 to length + t = asc(mid(text, i, 1)) + SendData(t) + next +end + +sub LCD_Cls() + SendCommand(LCD_CLEAR_DISPLAY) + SendCommand(LCD_CURSOR_HOME) +end + +sub LCD_Off() + SendCommand(LCD_SET_DISPLAY BOR LCD_DISPLAY_OFF) +end + +sub LCD_On() + SendCommand(LCD_SET_DISPLAY BOR LCD_DISPLAY_ON) +end + +sub LCD_Locate(x, y) + local dat + + if(x < 1) then x == 1 + + select case y + case 1 ' 1. line + dat = LCD_SET_DDADR + LCD_DDADR_LINE1 + x - 1 + case 2 ' 2. line + dat = LCD_SET_DDADR + LCD_DDADR_LINE2 + x - 1 + case 3 ' 3. line + dat = LCD_SET_DDADR + LCD_DDADR_LINE3 + x - 1 + case 4 ' 4. line + dat = LCD_SET_DDADR + LCD_DDADR_LINE4 + x - 1 + case else + return + end select + + SendCommand(dat) +end + +sub SendCommand(cmd) + RS.write(0) + SendByte(cmd) +end + +sub SendData(dat) + RS.write(1) + SendByte(dat) +end + +sub SendEnable() + E.write(1) + delay(1) + E.write(0) + delay(1) +end + +sub SendByte(byte) + Send4Bit(byte) ' Send high bits first + Send4Bit(byte lshift 4) ' Send low bits +end + +sub Send4Bit(byte) + DB7.write(GetBit(byte, 7)) + DB6.write(GetBit(byte, 6)) + DB5.write(GetBit(byte, 5)) + DB4.write(GetBit(byte, 4)) + SendEnable() +end + +func GetBit(value, bit) + return (value rshift bit) BAND 1 +end \ No newline at end of file From 385e8cbec451b97550f0f79af470924d7d3a88ff Mon Sep 17 00:00:00 2001 From: chrisws Date: Tue, 3 Sep 2024 21:43:51 +0930 Subject: [PATCH 053/131] Update dependencies --- nuklear/Nuklear | 2 +- raylib/README.md | 31 +++++--- raylib/func-def.h | 1 + raylib/func.h | 19 ++++- raylib/proc-def.h | 8 ++ raylib/proc.h | 188 ++++++++++++++++++++++++++++++++++++++++----- raylib/raygui | 2 +- raylib/raylib | 2 +- websocket/mongoose | 2 +- 9 files changed, 221 insertions(+), 34 deletions(-) diff --git a/nuklear/Nuklear b/nuklear/Nuklear index 504f831..f7847e6 160000 --- a/nuklear/Nuklear +++ b/nuklear/Nuklear @@ -1 +1 @@ -Subproject commit 504f8319a8211a24b49f68330b7a8962d8e4632c +Subproject commit f7847e602440a45fc1993aa88cd0f4d81ec881e8 diff --git a/raylib/README.md b/raylib/README.md index e4ffd64..7fe7882 100644 --- a/raylib/README.md +++ b/raylib/README.md @@ -1,10 +1,10 @@ -*Raylib* _MAJOR 5 _MINOR 1 _PATCH 0 5.1-dev +*Raylib* _MAJOR 5 _MINOR 5 _PATCH 0 5.5-dev ======= raylib is a simple and easy-to-use library to enjoy videogames programming. https://www.raylib.com/ -Implemented APIs (617) +Implemented APIs (626) ---------------- | Name | Description | @@ -57,7 +57,7 @@ Implemented APIs (617) | func DirectoryExists(dirPath) | Check if a directory path exists | | sub DisableCursor() | Disables cursor (lock cursor) | | sub DisableEventWaiting() | Disable waiting for events on EndDrawing(), automatic events polling | -| sub DrawBillboard(camera, texture, position, size, tint) | Draw a billboard texture | +| sub DrawBillboard(camera, texture, position, scale, tint) | Draw a billboard texture | | sub DrawBillboardPro(camera, texture, source, position, up, size, origin, rotation, tint) | Draw a billboard texture defined by source and rotation | | sub DrawBillboardRec(camera, texture, source, position, size, tint) | Draw a billboard texture defined by source | | sub DrawBoundingBox(box, color) | Draw bounding box (wires) | @@ -65,7 +65,7 @@ Implemented APIs (617) | sub DrawCapsuleWires(startPos, endPos, radius, slices, rings, color) | Draw capsule wireframe with the center of its sphere caps at startPos and endPos | | sub DrawCircle(centerX, centerY, radius, color) | Draw a color-filled circle | | sub DrawCircle3D(center, radius, rotationAxis, rotationAngle, color) | Draw a circle in 3D world space | -| sub DrawCircleGradient(centerX, centerY, radius, color1, color2) | Draw a gradient-filled circle | +| sub DrawCircleGradient(centerX, centerY, radius, inner, outer) | Draw a gradient-filled circle | | sub DrawCircleLines(centerX, centerY, radius, color) | Draw circle outline | | sub DrawCircleLinesV(center, radius, color) | Draw circle outline (Vector version) | | sub DrawCircleSector(center, radius, startAngle, endAngle, segments, color) | Draw a piece of a circle | @@ -91,6 +91,8 @@ Implemented APIs (617) | sub DrawLineV(startPos, endPos, color) | Draw a line (using gl lines) | | sub DrawModel(model, position, scale, tint) | Draw a model (with texture if set) | | sub DrawModelEx(model, position, rotationAxis, rotationAngle, scale, tint) | Draw a model with extended parameters | +| sub DrawModelPoints(model, position, scale, tint) | Draw a model as points | +| sub DrawModelPointsEx(model, position, rotationAxis, rotationAngle, scale, tint) | Draw a model as points with extended parameters | | sub DrawModelWires(model, position, scale, tint) | Draw a model wires (with texture if set) | | sub DrawModelWiresEx(model, position, rotationAxis, rotationAngle, scale, tint) | Draw a model wires (with texture if set) with extended parameters | | sub DrawPixel(posX, posY, color) | Draw a pixel | @@ -102,9 +104,9 @@ Implemented APIs (617) | sub DrawPolyLinesEx(center, sides, radius, rotation, lineThick, color) | Draw a polygon outline of n sides with extended parameters | | sub DrawRay(ray, color) | Draw a ray line | | sub DrawRectangle(posX, posY, width, height, color) | Draw a color-filled rectangle | -| sub DrawRectangleGradientEx(rec, col1, col2, col3, col4) | Draw a gradient-filled rectangle with custom vertex colors | -| sub DrawRectangleGradientH(posX, posY, width, height, color1, color2) | Draw a horizontal-gradient-filled rectangle | -| sub DrawRectangleGradientV(posX, posY, width, height, color1, color2) | Draw a vertical-gradient-filled rectangle | +| sub DrawRectangleGradientEx(rec, topLeft, bottomLeft, topRight, bottomRight) | Draw a gradient-filled rectangle with custom vertex colors | +| sub DrawRectangleGradientH(posX, posY, width, height, left, right) | Draw a horizontal-gradient-filled rectangle | +| sub DrawRectangleGradientV(posX, posY, width, height, top, bottom) | Draw a vertical-gradient-filled rectangle | | sub DrawRectangleLines(posX, posY, width, height, color) | Draw rectangle outline | | sub DrawRectangleLinesEx(rec, lineThick, color) | Draw rectangle outline with extended parameters | | sub DrawRectanglePro(rec, origin, rotation, color) | Draw a color-filled rectangle with pro parameters | @@ -347,6 +349,7 @@ Implemented APIs (617) | sub ImageDrawCircleLinesV(dst, center, radius, color) | Draw circle outline within an image (Vector version) | | sub ImageDrawCircleV(dst, center, radius, color) | Draw a filled circle within an image (Vector version) | | sub ImageDrawLine(dst, startPosX, startPosY, endPosX, endPosY, color) | Draw line within an image | +| sub ImageDrawLineEx(dst, start, end, thick, color) | Draw a line defining thickness within an image | | sub ImageDrawLineV(dst, start, end, color) | Draw line within an image (Vector version) | | sub ImageDrawPixel(dst, posX, posY, color) | Draw pixel within an image | | sub ImageDrawPixelV(dst, position, color) | Draw pixel within an image (Vector version) | @@ -356,11 +359,17 @@ Implemented APIs (617) | sub ImageDrawRectangleV(dst, position, size, color) | Draw rectangle within an image (Vector version) | | sub ImageDrawText(dst, text, posX, posY, fontSize, color) | Draw text (using default font) within an image (destination) | | sub ImageDrawTextEx(dst, font, text, position, fontSize, spacing, tint) | Draw text (custom sprite font) within an image (destination) | +| sub ImageDrawTriangle(dst, v1, v2, v3, color) | Draw triangle within an image | +| sub ImageDrawTriangleEx(dst, v1, v2, v3, c1, c2, c3) | Draw triangle with interpolated colors within an image | +| sub ImageDrawTriangleFan(dst, points, pointCount, color) | Draw a triangle fan defined by points within an image (first vertex is the center) | +| sub ImageDrawTriangleLines(dst, v1, v2, v3, color) | Draw triangle outline within an image | +| sub ImageDrawTriangleStrip(dst, points, pointCount, color) | Draw a triangle strip defined by points within an image | | sub ImageFlipHorizontal(image) | Flip image horizontally | | sub ImageFlipVertical(image) | Flip image vertically | | sub ImageFormat(image, newFormat) | Convert image data to desired format | +| func ImageFromChannel(image, selectedChannel) | Create an image from a selected channel of another image (GRAYSCALE) | | func ImageFromImage(image, rec) | Create an image from another image piece | -| sub ImageKernelConvolution(image, kernel, kernelSize) | Apply Custom Square image convolution kernel | +| sub ImageKernelConvolution(image, kernel, kernelSize) | Apply custom square convolution kernel to image | | sub ImageMipmaps(image) | Compute all mipmap levels for a provided image | | sub ImageResize(image, newWidth, newHeight) | Resize image (Bicubic scaling algorithm) | | sub ImageResizeCanvas(image, newWidth, newHeight, offsetX, offsetY, fill) | Resize canvas and fill with color | @@ -428,7 +437,7 @@ Implemented APIs (617) | func LoadFileData(fileName, dataSize) | Load file data as byte array (read) | | func LoadFileText(fileName) | Load text data from file (read), returns a '\\0' terminated string | | func LoadFont(fileName) | Load font from file into GPU memory (VRAM) | -| func LoadFontEx(fileName, fontSize, codepoints, codepointCount) | Load font from file with extended parameters, use NULL for codepoints and 0 for codepointCount to load the default character setFont | +| func LoadFontEx(fileName, fontSize, codepoints, codepointCount) | Load font from file with extended parameters | | func LoadFontFromImage(image, key, firstChar) | Load font from Image (XNA style) | | func LoadFontFromMemory(fileType, fileData, dataSize, fontSize, codepoints, codepointCount) | Load font from memory buffer, fileType refers to extension: i.e. '.ttf' | | func LoadImage(fileName) | Load image from file into CPU memory (RAM) | @@ -583,8 +592,8 @@ Implemented APIs (617) | func TextToPascal(text) | Get Pascal case notation version of provided string | | func TextToSnake(text) | Get Snake case notation version of provided string | | func TextToUpper(text) | Get upper case version of provided string | -| sub ToggleBorderlessWindowed() | Toggle window state: borderless windowed (only PLATFORM_DESKTOP) | -| sub ToggleFullscreen() | Toggle window state: fullscreen/windowed (only PLATFORM_DESKTOP) | +| sub ToggleBorderlessWindowed() | Toggle window state: borderless windowed [resizes window to match monitor resolution] (only PLATFORM_DESKTOP) | +| sub ToggleFullscreen() | Toggle window state: fullscreen/windowed [resizes monitor to match window resolution] (only PLATFORM_DESKTOP) | | sub UnloadAudioStream(stream) | Unload audio stream and free memory | | sub UnloadAutomationEventList(list) | Unload automation events list from file | | sub UnloadCodepoints(codepoints) | Unload codepoints data from memory | diff --git a/raylib/func-def.h b/raylib/func-def.h index bcc7597..a81906f 100644 --- a/raylib/func-def.h +++ b/raylib/func-def.h @@ -154,6 +154,7 @@ {2, 2, "GETWORLDTOSCREEN2D", cmd_getworldtoscreen2d}, {4, 4, "GETWORLDTOSCREENEX", cmd_getworldtoscreenex}, {1, 1, "IMAGECOPY", cmd_imagecopy}, + {2, 2, "IMAGEFROMCHANNEL", cmd_imagefromchannel}, {2, 2, "IMAGEFROMIMAGE", cmd_imagefromimage}, {3, 3, "IMAGETEXT", cmd_imagetext}, {5, 5, "IMAGETEXTEX", cmd_imagetextex}, diff --git a/raylib/func.h b/raylib/func.h index cd252fe..34f717b 100644 --- a/raylib/func.h +++ b/raylib/func.h @@ -1813,6 +1813,23 @@ static int cmd_imagecopy(int argc, slib_par_t *params, var_t *retval) { return result; } +// +// Create an image from a selected channel of another image (GRAYSCALE) +// +static int cmd_imagefromchannel(int argc, slib_par_t *params, var_t *retval) { + int result; + int image_id = get_image_id(argc, params, 0, retval); + if (image_id != -1) { + auto selectedChannel = get_param_int(argc, params, 1, 0); + auto fnResult = ImageFromChannel(_imageMap.at(image_id), selectedChannel); + v_setimage(retval, fnResult); + result = 1; + } else { + result = 0; + } + return result; +} + // // Create an image from another image piece // @@ -2489,7 +2506,7 @@ static int cmd_loadfont(int argc, slib_par_t *params, var_t *retval) { } // -// Load font from file with extended parameters, use NULL for codepoints and 0 for codepointCount to load the default character setFont +// Load font from file with extended parameters // static int cmd_loadfontex(int argc, slib_par_t *params, var_t *retval) { auto fileName = get_param_str(argc, params, 0, 0); diff --git a/raylib/proc-def.h b/raylib/proc-def.h index 470380a..6fe374c 100644 --- a/raylib/proc-def.h +++ b/raylib/proc-def.h @@ -45,6 +45,8 @@ {3, 3, "DRAWLINEV", cmd_drawlinev}, {4, 4, "DRAWMODEL", cmd_drawmodel}, {6, 6, "DRAWMODELEX", cmd_drawmodelex}, + {4, 4, "DRAWMODELPOINTS", cmd_drawmodelpoints}, + {6, 6, "DRAWMODELPOINTSEX", cmd_drawmodelpointsex}, {4, 4, "DRAWMODELWIRES", cmd_drawmodelwires}, {6, 6, "DRAWMODELWIRESEX", cmd_drawmodelwiresex}, {3, 3, "DRAWPIXEL", cmd_drawpixel}, @@ -132,6 +134,7 @@ {4, 4, "IMAGEDRAWCIRCLELINESV", cmd_imagedrawcirclelinesv}, {4, 4, "IMAGEDRAWCIRCLEV", cmd_imagedrawcirclev}, {6, 6, "IMAGEDRAWLINE", cmd_imagedrawline}, + {5, 5, "IMAGEDRAWLINEEX", cmd_imagedrawlineex}, {4, 4, "IMAGEDRAWLINEV", cmd_imagedrawlinev}, {4, 4, "IMAGEDRAWPIXEL", cmd_imagedrawpixel}, {3, 3, "IMAGEDRAWPIXELV", cmd_imagedrawpixelv}, @@ -141,6 +144,11 @@ {4, 4, "IMAGEDRAWRECTANGLEV", cmd_imagedrawrectanglev}, {6, 6, "IMAGEDRAWTEXT", cmd_imagedrawtext}, {7, 7, "IMAGEDRAWTEXTEX", cmd_imagedrawtextex}, + {5, 5, "IMAGEDRAWTRIANGLE", cmd_imagedrawtriangle}, + {7, 7, "IMAGEDRAWTRIANGLEEX", cmd_imagedrawtriangleex}, + {4, 4, "IMAGEDRAWTRIANGLEFAN", cmd_imagedrawtrianglefan}, + {5, 5, "IMAGEDRAWTRIANGLELINES", cmd_imagedrawtrianglelines}, + {4, 4, "IMAGEDRAWTRIANGLESTRIP", cmd_imagedrawtrianglestrip}, {1, 1, "IMAGEFLIPHORIZONTAL", cmd_imagefliphorizontal}, {1, 1, "IMAGEFLIPVERTICAL", cmd_imageflipvertical}, {2, 2, "IMAGEFORMAT", cmd_imageformat}, diff --git a/raylib/proc.h b/raylib/proc.h index 52aa170..fec3a31 100644 --- a/raylib/proc.h +++ b/raylib/proc.h @@ -128,9 +128,9 @@ static int cmd_drawbillboard(int argc, slib_par_t *params, var_t *retval) { if (texture_id != -1) { auto camera = get_camera_3d(argc, params, 0); auto position = get_param_vec3(argc, params, 2); - auto size = get_param_num(argc, params, 3, 0); + auto scale = get_param_num(argc, params, 3, 0); auto tint = get_param_color(argc, params, 4); - DrawBillboard(camera, _textureMap.at(texture_id), position, size, tint); + DrawBillboard(camera, _textureMap.at(texture_id), position, scale, tint); result = 1; } else { result = 0; @@ -251,9 +251,9 @@ static int cmd_drawcirclegradient(int argc, slib_par_t *params, var_t *retval) { auto centerX = get_param_int(argc, params, 0, 0); auto centerY = get_param_int(argc, params, 1, 0); auto radius = get_param_num(argc, params, 2, 0); - auto color1 = get_param_color(argc, params, 3); - auto color2 = get_param_color(argc, params, 4); - DrawCircleGradient(centerX, centerY, radius, color1, color2); + auto inner = get_param_color(argc, params, 3); + auto outer = get_param_color(argc, params, 4); + DrawCircleGradient(centerX, centerY, radius, inner, outer); return 1; } @@ -577,6 +577,44 @@ static int cmd_drawmodelex(int argc, slib_par_t *params, var_t *retval) { return result; } +// +// Draw a model as points +// +static int cmd_drawmodelpoints(int argc, slib_par_t *params, var_t *retval) { + int result; + int model_id = get_model_id(argc, params, 0, retval); + if (model_id != -1) { + auto position = get_param_vec3(argc, params, 1); + auto scale = get_param_num(argc, params, 2, 0); + auto tint = get_param_color(argc, params, 3); + DrawModelPoints(_modelMap.at(model_id), position, scale, tint); + result = 1; + } else { + result = 0; + } + return result; +} + +// +// Draw a model as points with extended parameters +// +static int cmd_drawmodelpointsex(int argc, slib_par_t *params, var_t *retval) { + int result; + int model_id = get_model_id(argc, params, 0, retval); + if (model_id != -1) { + auto position = get_param_vec3(argc, params, 1); + auto rotationAxis = get_param_vec3(argc, params, 2); + auto rotationAngle = get_param_num(argc, params, 3, 0); + auto scale = get_param_vec3(argc, params, 4); + auto tint = get_param_color(argc, params, 5); + DrawModelPointsEx(_modelMap.at(model_id), position, rotationAxis, rotationAngle, scale, tint); + result = 1; + } else { + result = 0; + } + return result; +} + // // Draw a model wires (with texture if set) // @@ -725,11 +763,11 @@ static int cmd_drawrectangle(int argc, slib_par_t *params, var_t *retval) { // static int cmd_drawrectanglegradientex(int argc, slib_par_t *params, var_t *retval) { auto rec = get_param_rect(argc, params, 0); - auto col1 = get_param_color(argc, params, 1); - auto col2 = get_param_color(argc, params, 2); - auto col3 = get_param_color(argc, params, 3); - auto col4 = get_param_color(argc, params, 4); - DrawRectangleGradientEx(rec, col1, col2, col3, col4); + auto topLeft = get_param_color(argc, params, 1); + auto bottomLeft = get_param_color(argc, params, 2); + auto topRight = get_param_color(argc, params, 3); + auto bottomRight = get_param_color(argc, params, 4); + DrawRectangleGradientEx(rec, topLeft, bottomLeft, topRight, bottomRight); return 1; } @@ -741,9 +779,9 @@ static int cmd_drawrectanglegradienth(int argc, slib_par_t *params, var_t *retva auto posY = get_param_int(argc, params, 1, 0); auto width = get_param_int(argc, params, 2, 0); auto height = get_param_int(argc, params, 3, 0); - auto color1 = get_param_color(argc, params, 4); - auto color2 = get_param_color(argc, params, 5); - DrawRectangleGradientH(posX, posY, width, height, color1, color2); + auto left = get_param_color(argc, params, 4); + auto right = get_param_color(argc, params, 5); + DrawRectangleGradientH(posX, posY, width, height, left, right); return 1; } @@ -755,9 +793,9 @@ static int cmd_drawrectanglegradientv(int argc, slib_par_t *params, var_t *retva auto posY = get_param_int(argc, params, 1, 0); auto width = get_param_int(argc, params, 2, 0); auto height = get_param_int(argc, params, 3, 0); - auto color1 = get_param_color(argc, params, 4); - auto color2 = get_param_color(argc, params, 5); - DrawRectangleGradientV(posX, posY, width, height, color1, color2); + auto top = get_param_color(argc, params, 4); + auto bottom = get_param_color(argc, params, 5); + DrawRectangleGradientV(posX, posY, width, height, top, bottom); return 1; } @@ -1770,6 +1808,25 @@ static int cmd_imagedrawline(int argc, slib_par_t *params, var_t *retval) { return result; } +// +// Draw a line defining thickness within an image +// +static int cmd_imagedrawlineex(int argc, slib_par_t *params, var_t *retval) { + int result; + int dst_id = get_image_id(argc, params, 0, retval); + if (dst_id != -1) { + auto start = get_param_vec2(argc, params, 1); + auto end = get_param_vec2(argc, params, 2); + auto thick = get_param_int(argc, params, 3, 0); + auto color = get_param_color(argc, params, 4); + ImageDrawLineEx(&_imageMap.at(dst_id), start, end, thick, color); + result = 1; + } else { + result = 0; + } + return result; +} + // // Draw line within an image (Vector version) // @@ -1937,6 +1994,101 @@ static int cmd_imagedrawtextex(int argc, slib_par_t *params, var_t *retval) { return result; } +// +// Draw triangle within an image +// +static int cmd_imagedrawtriangle(int argc, slib_par_t *params, var_t *retval) { + int result; + int dst_id = get_image_id(argc, params, 0, retval); + if (dst_id != -1) { + auto v1 = get_param_vec2(argc, params, 1); + auto v2 = get_param_vec2(argc, params, 2); + auto v3 = get_param_vec2(argc, params, 3); + auto color = get_param_color(argc, params, 4); + ImageDrawTriangle(&_imageMap.at(dst_id), v1, v2, v3, color); + result = 1; + } else { + result = 0; + } + return result; +} + +// +// Draw triangle with interpolated colors within an image +// +static int cmd_imagedrawtriangleex(int argc, slib_par_t *params, var_t *retval) { + int result; + int dst_id = get_image_id(argc, params, 0, retval); + if (dst_id != -1) { + auto v1 = get_param_vec2(argc, params, 1); + auto v2 = get_param_vec2(argc, params, 2); + auto v3 = get_param_vec2(argc, params, 3); + auto c1 = get_param_color(argc, params, 4); + auto c2 = get_param_color(argc, params, 5); + auto c3 = get_param_color(argc, params, 6); + ImageDrawTriangleEx(&_imageMap.at(dst_id), v1, v2, v3, c1, c2, c3); + result = 1; + } else { + result = 0; + } + return result; +} + +// +// Draw a triangle fan defined by points within an image (first vertex is the center) +// +static int cmd_imagedrawtrianglefan(int argc, slib_par_t *params, var_t *retval) { + int result; + int dst_id = get_image_id(argc, params, 0, retval); + if (dst_id != -1) { + auto points = (Vector2 *)get_param_vec2_array(argc, params, 1); + auto pointCount = get_param_int(argc, params, 2, 0); + auto color = get_param_color(argc, params, 3); + ImageDrawTriangleFan(&_imageMap.at(dst_id), points, pointCount, color); + result = 1; + } else { + result = 0; + } + return result; +} + +// +// Draw triangle outline within an image +// +static int cmd_imagedrawtrianglelines(int argc, slib_par_t *params, var_t *retval) { + int result; + int dst_id = get_image_id(argc, params, 0, retval); + if (dst_id != -1) { + auto v1 = get_param_vec2(argc, params, 1); + auto v2 = get_param_vec2(argc, params, 2); + auto v3 = get_param_vec2(argc, params, 3); + auto color = get_param_color(argc, params, 4); + ImageDrawTriangleLines(&_imageMap.at(dst_id), v1, v2, v3, color); + result = 1; + } else { + result = 0; + } + return result; +} + +// +// Draw a triangle strip defined by points within an image +// +static int cmd_imagedrawtrianglestrip(int argc, slib_par_t *params, var_t *retval) { + int result; + int dst_id = get_image_id(argc, params, 0, retval); + if (dst_id != -1) { + auto points = (Vector2 *)get_param_vec2_array(argc, params, 1); + auto pointCount = get_param_int(argc, params, 2, 0); + auto color = get_param_color(argc, params, 3); + ImageDrawTriangleStrip(&_imageMap.at(dst_id), points, pointCount, color); + result = 1; + } else { + result = 0; + } + return result; +} + // // Flip image horizontally // @@ -2981,7 +3133,7 @@ static int cmd_textappend(int argc, slib_par_t *params, var_t *retval) { } // -// Toggle window state: borderless windowed (only PLATFORM_DESKTOP) +// Toggle window state: borderless windowed [resizes window to match monitor resolution] (only PLATFORM_DESKTOP) // static int cmd_toggleborderlesswindowed(int argc, slib_par_t *params, var_t *retval) { ToggleBorderlessWindowed(); @@ -2989,7 +3141,7 @@ static int cmd_toggleborderlesswindowed(int argc, slib_par_t *params, var_t *ret } // -// Toggle window state: fullscreen/windowed (only PLATFORM_DESKTOP) +// Toggle window state: fullscreen/windowed [resizes monitor to match window resolution] (only PLATFORM_DESKTOP) // static int cmd_togglefullscreen(int argc, slib_par_t *params, var_t *retval) { ToggleFullscreen(); diff --git a/raylib/raygui b/raylib/raygui index 4b3d94f..6aae838 160000 --- a/raylib/raygui +++ b/raylib/raygui @@ -1 +1 @@ -Subproject commit 4b3d94f5df6a5a2aa86286350f7e20c0ca35f516 +Subproject commit 6aae83894ac20b7ac8bf511f43d368f0ad409534 diff --git a/raylib/raylib b/raylib/raylib index b4fbdc0..59b44a4 160000 --- a/raylib/raylib +++ b/raylib/raylib @@ -1 +1 @@ -Subproject commit b4fbdc028302f9a697f196e8d02a7dca28912f59 +Subproject commit 59b44a4908e830bca755af3d83d25a64f6d54bd2 diff --git a/websocket/mongoose b/websocket/mongoose index 5adbadc..bdc009e 160000 --- a/websocket/mongoose +++ b/websocket/mongoose @@ -1 +1 @@ -Subproject commit 5adbadc9010a6c8416374b7ee3bc2789428a3e4e +Subproject commit bdc009eb6537f3d6b24a348a961e531dfc04e661 From a2646bd2044c914f5c7e8308d997be5b1a2de728 Mon Sep 17 00:00:00 2001 From: chrisws Date: Wed, 25 Sep 2024 20:01:25 +0930 Subject: [PATCH 054/131] Minor performance fixes which ultimately didn't help SPI --- include/javaproxy.h | 19 +++++++----- .../main/java/ioio/smallbasic/IOIOImpl.java | 5 --- .../main/java/ioio/smallbasic/IOService.java | 4 +-- .../java/ioio/smallbasic/SpiMasterImpl.java | 16 +++++----- .../main/java/ioio/smallbasic/TimerUtil.java | 21 ------------- .../pc/SerialPortIOIOConnection.java | 2 +- ioio/main.cpp | 31 +++++++++++-------- 7 files changed, 40 insertions(+), 58 deletions(-) delete mode 100644 ioio/ioio/src/main/java/ioio/smallbasic/TimerUtil.java diff --git a/include/javaproxy.h b/include/javaproxy.h index d6386a5..c6de137 100644 --- a/include/javaproxy.h +++ b/include/javaproxy.h @@ -16,8 +16,6 @@ JNIEnv *g_env; JavaVM *g_jvm; jobject g_activity; -#define ARRAY_SIZE 254 - #if defined(ANDROID_MODULE) #define attachCurrentThread() g_jvm->AttachCurrentThread(&g_env, nullptr) #define detachCurrentThread() g_jvm->DetachCurrentThread() @@ -73,7 +71,8 @@ struct JavaProxy { JavaProxy(): _clazz(nullptr), _instance(nullptr), - _array(nullptr) { + _array(nullptr), + _arraySize(0) { } virtual ~JavaProxy() { @@ -286,23 +285,28 @@ struct JavaProxy { } // populate the java byte array with the contents of the basic array - int populateByteArray(int argc, slib_par_t *params, int offset) { + int populateByteArray(int argc, slib_par_t *params, int offset, int arraySize) { int result; + if (_array && _arraySize < arraySize) { + g_env->DeleteLocalRef(_array); + _array = nullptr; + } if (!_array) { - _array = g_env->NewByteArray(ARRAY_SIZE); + _array = g_env->NewByteArray(arraySize); + _arraySize = arraySize; } jbyte *elements = g_env->GetByteArrayElements(_array, nullptr); if ((argc - offset) == 1 && is_param_array(argc, params, offset)) { // argument is an array (assume of ints) var_s *array = params[offset].var_p; int size = v_asize(array); - for (int i = 0; i < size && i < ARRAY_SIZE; i++) { + for (int i = 0; i < size && i < arraySize; i++) { var_s *elem = v_elem(array, i); elements[i] = v_is_type(elem, V_INT) ? elem->v.i : elem->v.n; } result = size; } else { - for (int i = offset, j = 0; i < argc && i < ARRAY_SIZE; i++, j++) { + for (int i = offset, j = 0; i < argc && i < arraySize; i++, j++) { elements[j] = get_param_int(argc, params, i, 0); } result = argc - offset; @@ -316,6 +320,7 @@ struct JavaProxy { jclass _clazz; jobject _instance; jbyteArray _array; + int _arraySize; }; #if defined(ANDROID_MODULE) diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/IOIOImpl.java b/ioio/ioio/src/main/java/ioio/smallbasic/IOIOImpl.java index d09c0db..0f3d455 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/IOIOImpl.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/IOIOImpl.java @@ -61,11 +61,6 @@ public void sync() { public void waitForConnect(int latency) { IOUtil.setError(null); - if (latency < 0) { - IOUtil.setHardReset(true); - } else { - TimerUtil.setLatency(latency); - } IOService.getInstance().start(); handleError(); lock.invoke(IOIO::waitForConnect); diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/IOService.java b/ioio/ioio/src/main/java/ioio/smallbasic/IOService.java index 8d3e239..da025fd 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/IOService.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/IOService.java @@ -86,7 +86,6 @@ private void registerPin(int pin) throws IOException { public class IOServiceLooper implements IOIOLooper { private IOIO ioio; - private long lastAccessMillis; @Override public void disconnected() { @@ -106,7 +105,7 @@ public void incompatible(IOIO ioio) { @Override public void loop() throws ConnectionLostException, InterruptedException { - lastAccessMillis = TimerUtil.tick(lastAccessMillis); + Thread.sleep(0, 5); for (IOTask next: ioTasks) { try { next.loop(); @@ -120,7 +119,6 @@ public void loop() throws ConnectionLostException, InterruptedException { @Override public void setup(IOIO ioio) { this.ioio = ioio; - this.lastAccessMillis = System.currentTimeMillis(); for (IOTask next: ioTasks) { try { next.setup(ioio); diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/SpiMasterImpl.java b/ioio/ioio/src/main/java/ioio/smallbasic/SpiMasterImpl.java index 5350b3d..4ab01b3 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/SpiMasterImpl.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/SpiMasterImpl.java @@ -9,7 +9,7 @@ public class SpiMasterImpl extends IOTask { private static final String TAG = "SpiMasterImpl"; - private static final int SPI_WRITE_MAX = 63; + private static final int SPI_WRITE_MAX = 62; private final byte[] BATCH = new byte[SPI_WRITE_MAX]; private final IOLock lock = new IOLock<>(); private SpiMaster spiMaster = null; @@ -57,16 +57,16 @@ public long readWrite(int readLen, final byte[] write, int writeLen) { public void write(final byte[] write, int writeLen) { handleError(); lock.invoke((i) -> { - if (writeLen < SPI_WRITE_MAX) { - spiMaster.writeRead(write, writeLen, writeLen, null, 0); - } else { + if (writeLen > SPI_WRITE_MAX) { int srcPos = 0; while (srcPos < writeLen) { - int batchLen = Math.min(writeLen - srcPos, SPI_WRITE_MAX); + int batchLen = Math.min(writeLen - srcPos, 2); System.arraycopy(write, srcPos, BATCH, 0, batchLen); - spiMaster.writeRead(BATCH, batchLen, batchLen, null, 0); - srcPos += SPI_WRITE_MAX; + spiMaster.writeReadAsync(0, BATCH, batchLen, batchLen, null, 0); + srcPos += batchLen; } + } else { + spiMaster.writeRead(write, writeLen, writeLen, null, 0); } }); } @@ -79,7 +79,7 @@ void loop() throws ConnectionLostException, InterruptedException { @Override void setup(IOIO ioio) throws ConnectionLostException { Log.i(TAG, "setup entered: miso:" + miso + " mosi:" + mosi + " clk:" + clk + " cs:" + slaveSelect); - spiMaster = ioio.openSpiMaster(miso, mosi, clk, slaveSelect, SpiMaster.Rate.RATE_1M); + spiMaster = ioio.openSpiMaster(miso, mosi, clk, slaveSelect, SpiMaster.Rate.RATE_4M); } private void pinError(String name) { diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/TimerUtil.java b/ioio/ioio/src/main/java/ioio/smallbasic/TimerUtil.java deleted file mode 100644 index 4689f33..0000000 --- a/ioio/ioio/src/main/java/ioio/smallbasic/TimerUtil.java +++ /dev/null @@ -1,21 +0,0 @@ -package ioio.smallbasic; - -public class TimerUtil { - private static int latency = 50; - - private TimerUtil() { - // no access - } - - public static void setLatency(int latency) { - TimerUtil.latency = latency; - } - - public static long tick(long lastAccessMillis) throws InterruptedException { - long interval = System.currentTimeMillis() - lastAccessMillis; - if (latency > 0 && interval < latency) { - Thread.sleep(latency - interval); - } - return System.currentTimeMillis(); - } -} diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/pc/SerialPortIOIOConnection.java b/ioio/ioio/src/main/java/ioio/smallbasic/pc/SerialPortIOIOConnection.java index 23fcda4..eb1163c 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/pc/SerialPortIOIOConnection.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/pc/SerialPortIOIOConnection.java @@ -33,7 +33,7 @@ public boolean canClose() { @Override synchronized public void disconnect() { abort = true; - if (serialPort != null) { + if (serialPort != null && inputStream != null) { try { inputStream.close(); } catch (IOException e) { diff --git a/ioio/main.cpp b/ioio/main.cpp index fb934dd..d8852c5 100644 --- a/ioio/main.cpp +++ b/ioio/main.cpp @@ -25,8 +25,8 @@ #define CLASS_SPIMASTER "ioio/smallbasic/SpiMasterImpl" #define CLASS_IOIO "ioio/smallbasic/IOIOImpl" #define CLASS_IOTASK_ID 1 -#define SPI_WRITE_MAX 63 -#define TWI_WRITE_MAX ARRAY_SIZE +#define SPI_WRITE_MAX 62 +#define TWI_WRITE_MAX 255 struct IOTask : JavaProxy { IOTask() : JavaProxy() { @@ -47,7 +47,7 @@ struct IOTask : JavaProxy { // int readWrite(bytes, byte[] write) { int invokeSpiReadWrite(int argc, slib_par_t *arg, var_s *retval) { int result = 0; - int writeLen = populateByteArray(argc, arg, 1); + int writeLen = populateByteArray(argc, arg, 1, SPI_WRITE_MAX); if (writeLen > SPI_WRITE_MAX) { error(retval, "write array", 1, SPI_WRITE_MAX); } else if (_instance != nullptr) { @@ -67,12 +67,17 @@ struct IOTask : JavaProxy { return result; } - // int write(byte[] write) { + // int write(byte[] write, int length) { int invokeSpiWrite(int argc, slib_par_t *arg, var_s *retval) { int result = 0; - int writeLen = populateByteArray(argc, arg, 0); - if (writeLen > ARRAY_SIZE) { - error(retval, "write array", 1, ARRAY_SIZE); + int maxSize = SPI_WRITE_MAX; + if (is_param_array(argc, arg, 0)) { + // allow an entire LCD to be updated within one IOIOLooper.loop() call + maxSize = v_asize(arg[0].var_p); + } + int writeLen = populateByteArray(argc, arg, 0, maxSize); + if (writeLen > maxSize) { + error(retval, "write array", 1, maxSize); } else if (_instance != nullptr) { attachCurrentThread(); jmethodID method = g_env->GetMethodID(_clazz, "write", "([BI)V"); @@ -90,7 +95,7 @@ struct IOTask : JavaProxy { // int readWrite(int address, byte[] write) { int invokeTwiReadWrite(int argc, slib_par_t *arg, var_s *retval) { int result = 0; - int writeLen = populateByteArray(argc, arg, 2); + int writeLen = populateByteArray(argc, arg, 2, TWI_WRITE_MAX); if (writeLen > TWI_WRITE_MAX) { error(retval, "write array", 1, TWI_WRITE_MAX); } else if (_instance != nullptr) { @@ -114,7 +119,7 @@ struct IOTask : JavaProxy { // int write(int address, byte[] write) { int invokeTwiWrite(int argc, slib_par_t *arg, var_s *retval) { int result = 0; - int writeLen = populateByteArray(argc, arg, 1); + int writeLen = populateByteArray(argc, arg, 1, TWI_WRITE_MAX); if (writeLen > TWI_WRITE_MAX) { error(retval, "write array", 1, TWI_WRITE_MAX); } else if (_instance != nullptr) { @@ -155,7 +160,7 @@ static int cmd_twimaster_readwrite(var_s *self, int argc, slib_par_t *arg, var_s int result = 0; auto readBytes = get_param_int(argc, arg, 1, 0); if (argc < 2) { - error(retval, "TwiMaster.readWrite(address, read-bytes, [data]", 2, ARRAY_SIZE); + error(retval, "TwiMaster.readWrite(address, read-bytes, [data]", 2, TWI_WRITE_MAX); } else if (readBytes < 1 || readBytes > 8) { error(retval, "read-bytes value out of range. Expected a number between 1 and 8"); } else { @@ -170,7 +175,7 @@ static int cmd_twimaster_readwrite(var_s *self, int argc, slib_par_t *arg, var_s static int cmd_twimaster_write(var_s *self, int argc, slib_par_t *arg, var_s *retval) { int result = 0; if (argc < 2) { - error(retval, "TwiMaster.write", 2, ARRAY_SIZE); + error(retval, "TwiMaster.write", 2, TWI_WRITE_MAX); } else { int id = get_io_class_id(self, retval); if (id != -1) { @@ -184,7 +189,7 @@ static int cmd_spimaster_readwrite(var_s *self, int argc, slib_par_t *arg, var_s int result = 0; auto readBytes = get_param_int(argc, arg, 0, 0); if (argc < 2) { - error(retval, "SpiMaster.readWrite(read-bytes, [data]", 2, ARRAY_SIZE); + error(retval, "SpiMaster.readWrite(read-bytes, [data]", 2, SPI_WRITE_MAX); } else if (readBytes < 1 || readBytes > 8) { error(retval, "read-bytes value out of range. Expected a number between 1 and 8"); } else { @@ -199,7 +204,7 @@ static int cmd_spimaster_readwrite(var_s *self, int argc, slib_par_t *arg, var_s static int cmd_spimaster_write(var_s *self, int argc, slib_par_t *arg, var_s *retval) { int result = 0; if (argc < 1) { - error(retval, "SpiMaster.write", 1, ARRAY_SIZE); + error(retval, "SpiMaster.write", 1, SPI_WRITE_MAX); } else { int id = get_io_class_id(self, retval); if (id != -1) { From 82be1e01e00a0e8221b91fda30532479792cf31d Mon Sep 17 00:00:00 2001 From: chrisws Date: Wed, 25 Sep 2024 20:02:14 +0930 Subject: [PATCH 055/131] Update dependencies --- nuklear/Nuklear | 2 +- raylib/README.md | 9 ++++--- raylib/func-def.h | 2 ++ raylib/func.h | 26 ++++++++++++++++++-- raylib/proc-def.h | 1 + raylib/proc.h | 17 +++++++++++++ raylib/raygui | 2 +- raylib/raylib | 2 +- raylib/samples/textures_image_generation.bas | 4 +-- websocket/mongoose | 2 +- 10 files changed, 56 insertions(+), 11 deletions(-) diff --git a/nuklear/Nuklear b/nuklear/Nuklear index f7847e6..ca8aaf3 160000 --- a/nuklear/Nuklear +++ b/nuklear/Nuklear @@ -1 +1 @@ -Subproject commit f7847e602440a45fc1993aa88cd0f4d81ec881e8 +Subproject commit ca8aaf3f65e9fc8ac5d90fefc1204585add4b89b diff --git a/raylib/README.md b/raylib/README.md index 7fe7882..2f30f4f 100644 --- a/raylib/README.md +++ b/raylib/README.md @@ -4,7 +4,7 @@ raylib is a simple and easy-to-use library to enjoy videogames programming. https://www.raylib.com/ -Implemented APIs (626) +Implemented APIs (629) ---------------- | Name | Description | @@ -43,6 +43,7 @@ Implemented APIs (626) | func ColorFromHSV(hue, saturation, value) | Get a Color from HSV values, hue [0..360], saturation/value [0..1] | | func ColorFromNormalized(normalized) | Get Color from normalized values [0..1] | | func ColorIsEqual(col1, col2) | Check if two colors are equal | +| func ColorLerp(color1, color2, factor) | Get color lerp interpolation between two colors, factor [0.0f..1.0f] | | func ColorNormalize(color) | Get Color normalized as float [0..1] | | func ColorTint(color, tint) | Get color multiplied with another color | | func ColorToHSV(color) | Get HSV values for a Color, hue [0..360], saturation/value [0..1] | @@ -432,12 +433,12 @@ Implemented APIs (626) | func LoadAutomationEventList(fileName) | Load automation events list from file, NULL for empty list, capacity = MAX_AUTOMATION_EVENTS | | func LoadCodepoints(text, count) | Load all codepoints from a UTF-8 text string, codepoints count returned by parameter | | func LoadDirectoryFiles(dirPath) | Load directory filepaths | -| func LoadDirectoryFilesEx(basePath, filter, scanSubdirs) | Load directory filepaths with extension filtering and recursive directory scan | +| func LoadDirectoryFilesEx(basePath, filter, scanSubdirs) | Load directory filepaths with extension filtering and recursive directory scan. Use 'DIR' in the filter string to include directories in the result | | func LoadDroppedFiles() | Load dropped filepaths | | func LoadFileData(fileName, dataSize) | Load file data as byte array (read) | | func LoadFileText(fileName) | Load text data from file (read), returns a '\\0' terminated string | | func LoadFont(fileName) | Load font from file into GPU memory (VRAM) | -| func LoadFontEx(fileName, fontSize, codepoints, codepointCount) | Load font from file with extended parameters | +| func LoadFontEx(fileName, fontSize, codepoints, codepointCount) | Load font from file with extended parameters, use NULL for codepoints and 0 for codepointCount to load the default character set, font size is provided in pixels height | | func LoadFontFromImage(image, key, firstChar) | Load font from Image (XNA style) | | func LoadFontFromMemory(fileType, fileData, dataSize, fontSize, codepoints, codepointCount) | Load font from memory buffer, fileType refers to extension: i.e. '.ttf' | | func LoadImage(fileName) | Load image from file into CPU memory (RAM) | @@ -469,6 +470,7 @@ Implemented APIs (626) | func LoadWave(fileName) | Load wave data from file | | func LoadWaveFromMemory(fileType, fileData, dataSize) | Load wave from memory buffer, fileType refers to extension: i.e. '.wav' | | func LoadWaveSamples(wave) | Load samples data from wave as a 32bit float data array | +| func MakeDirectory(dirPath) | Create directories (including full path requested), returns 0 on success | | sub MaximizeWindow() | Set window state: maximized, if resizable (only PLATFORM_DESKTOP) | | func MeasureText(text, fontSize) | Measure string width for default font | | func MeasureTextEx(font, text, fontSize, spacing) | Measure string size for Font | @@ -623,6 +625,7 @@ Implemented APIs (626) | sub UpdateCamera(camera, mode) | Update camera position for selected mode | | sub UpdateMeshBuffer(mesh, index, data, dataSize, offset) | Update mesh vertex data in GPU for a specific buffer index | | sub UpdateModelAnimation(model, anim, frame) | Update model animation pose | +| sub UpdateModelAnimationBoneMatrices(model, anim, frame) | Update model animation mesh bone matrices | | sub UpdateMusicStream(music) | Updates buffers for music streaming | | func updatePhysics() | n/a | | sub UpdateSound(sound, data, sampleCount) | Update sound buffer with new data | diff --git a/raylib/func-def.h b/raylib/func-def.h index a81906f..a3443cb 100644 --- a/raylib/func-def.h +++ b/raylib/func-def.h @@ -20,6 +20,7 @@ {3, 3, "COLORFROMHSV", cmd_colorfromhsv}, {1, 1, "COLORFROMNORMALIZED", cmd_colorfromnormalized}, {2, 2, "COLORISEQUAL", cmd_colorisequal}, + {3, 3, "COLORLERP", cmd_colorlerp}, {1, 1, "COLORNORMALIZE", cmd_colornormalize}, {2, 2, "COLORTINT", cmd_colortint}, {1, 1, "COLORTOHSV", cmd_colortohsv}, @@ -241,6 +242,7 @@ {1, 1, "LOADWAVE", cmd_loadwave}, {3, 3, "LOADWAVEFROMMEMORY", cmd_loadwavefrommemory}, {1, 1, "LOADWAVESAMPLES", cmd_loadwavesamples}, + {1, 1, "MAKEDIRECTORY", cmd_makedirectory}, {2, 2, "MEASURETEXT", cmd_measuretext}, {4, 4, "MEASURETEXTEX", cmd_measuretextex}, {1, 1, "MEMALLOC", cmd_memalloc}, diff --git a/raylib/func.h b/raylib/func.h index 34f717b..45d69af 100644 --- a/raylib/func.h +++ b/raylib/func.h @@ -258,6 +258,18 @@ static int cmd_colorisequal(int argc, slib_par_t *params, var_t *retval) { return 1; } +// +// Get color lerp interpolation between two colors, factor [0.0f..1.0f] +// +static int cmd_colorlerp(int argc, slib_par_t *params, var_t *retval) { + auto color1 = get_param_color(argc, params, 0); + auto color2 = get_param_color(argc, params, 1); + auto factor = get_param_num(argc, params, 2, 0); + auto fnResult = ColorLerp(color1, color2, factor); + v_setcolor(retval, fnResult); + return 1; +} + // // Get Color normalized as float [0..1] // @@ -2452,7 +2464,7 @@ static int cmd_loaddirectoryfiles(int argc, slib_par_t *params, var_t *retval) { } // -// Load directory filepaths with extension filtering and recursive directory scan +// Load directory filepaths with extension filtering and recursive directory scan. Use 'DIR' in the filter string to include directories in the result // static int cmd_loaddirectoryfilesex(int argc, slib_par_t *params, var_t *retval) { auto basePath = get_param_str(argc, params, 0, 0); @@ -2506,7 +2518,7 @@ static int cmd_loadfont(int argc, slib_par_t *params, var_t *retval) { } // -// Load font from file with extended parameters +// Load font from file with extended parameters, use NULL for codepoints and 0 for codepointCount to load the default character set, font size is provided in pixels height // static int cmd_loadfontex(int argc, slib_par_t *params, var_t *retval) { auto fileName = get_param_str(argc, params, 0, 0); @@ -2887,6 +2899,16 @@ static int cmd_loadwavesamples(int argc, slib_par_t *params, var_t *retval) { return result; } +// +// Create directories (including full path requested), returns 0 on success +// +static int cmd_makedirectory(int argc, slib_par_t *params, var_t *retval) { + auto dirPath = get_param_str(argc, params, 0, 0); + auto fnResult = MakeDirectory(dirPath); + v_setint(retval, fnResult); + return 1; +} + // // Measure string width for default font // diff --git a/raylib/proc-def.h b/raylib/proc-def.h index 6fe374c..ce15d14 100644 --- a/raylib/proc-def.h +++ b/raylib/proc-def.h @@ -262,6 +262,7 @@ {3, 3, "UPDATEAUDIOSTREAM", cmd_updateaudiostream}, {5, 5, "UPDATEMESHBUFFER", cmd_updatemeshbuffer}, {3, 3, "UPDATEMODELANIMATION", cmd_updatemodelanimation}, + {3, 3, "UPDATEMODELANIMATIONBONEMATRICES", cmd_updatemodelanimationbonematrices}, {1, 1, "UPDATEMUSICSTREAM", cmd_updatemusicstream}, {3, 3, "UPDATESOUND", cmd_updatesound}, {3, 3, "UPDATETEXTUREREC", cmd_updatetexturerec}, diff --git a/raylib/proc.h b/raylib/proc.h index fec3a31..0bf6dea 100644 --- a/raylib/proc.h +++ b/raylib/proc.h @@ -3509,6 +3509,23 @@ static int cmd_updatemodelanimation(int argc, slib_par_t *params, var_t *retval) return result; } +// +// Update model animation mesh bone matrices +// +static int cmd_updatemodelanimationbonematrices(int argc, slib_par_t *params, var_t *retval) { + int result; + int model_id = get_model_id(argc, params, 0, retval); + int anim_id = get_model_animation_id(argc, params, 1, retval); + if (model_id != -1 && anim_id != -1) { + auto frame = get_param_int(argc, params, 2, 0); + UpdateModelAnimationBoneMatrices(_modelMap.at(model_id), _modelAnimationMap.at(anim_id), frame); + result = 1; + } else { + result = 0; + } + return result; +} + // // Updates buffers for music streaming // diff --git a/raylib/raygui b/raylib/raygui index 6aae838..38bc79b 160000 --- a/raylib/raygui +++ b/raylib/raygui @@ -1 +1 @@ -Subproject commit 6aae83894ac20b7ac8bf511f43d368f0ad409534 +Subproject commit 38bc79b4322cfdbfb3166810f96c6f4f0f2b6cc8 diff --git a/raylib/raylib b/raylib/raylib index 59b44a4..13178b9 160000 --- a/raylib/raylib +++ b/raylib/raylib @@ -1 +1 @@ -Subproject commit 59b44a4908e830bca755af3d83d25a64f6d54bd2 +Subproject commit 13178b9373cebffb97a4ff66b734bacbb9d8758d diff --git a/raylib/samples/textures_image_generation.bas b/raylib/samples/textures_image_generation.bas index 4b25146..5ea9530 100644 --- a/raylib/samples/textures_image_generation.bas +++ b/raylib/samples/textures_image_generation.bas @@ -19,8 +19,8 @@ const screenHeight = 450 rl.InitWindow(screenWidth, screenHeight, "SmallBASIC/raylib [textures] example - procedural images generation") -verticalGradient = rl.GenImageGradientV(screenWidth, screenHeight, c.RED, c.BLUE) -horizontalGradient = rl.GenImageGradientH(screenWidth, screenHeight, c.RED, c.BLUE) +verticalGradient = rl.GenImageGradientLinear(screenWidth, screenHeight, 0, c.RED, c.BLUE) +horizontalGradient = rl.GenImageGradientLinear(screenWidth, screenHeight, 90, c.RED, c.BLUE) radialGradient = rl.GenImageGradientRadial(screenWidth, screenHeight, 0.0, c.WHITE, c.BLACK) checked = rl.GenImageChecked(screenWidth, screenHeight, 32, 32, c.RED, c.BLUE) whiteNoise = rl.GenImageWhiteNoise(screenWidth, screenHeight, 0.5) diff --git a/websocket/mongoose b/websocket/mongoose index bdc009e..40ec6b2 160000 --- a/websocket/mongoose +++ b/websocket/mongoose @@ -1 +1 @@ -Subproject commit bdc009eb6537f3d6b24a348a961e531dfc04e661 +Subproject commit 40ec6b263ddaf217104edfde220b8ae3cade039d From 7975a447203b13a3cdb77f49757441e0b63d1a3d Mon Sep 17 00:00:00 2001 From: chrisws Date: Thu, 31 Oct 2024 21:09:17 +1030 Subject: [PATCH 056/131] Some changes for testing Teensy4IoIoOtg --- .../main/java/ioio/smallbasic/IOIOImpl.java | 2 +- .../main/java/ioio/smallbasic/pc/HexDump.java | 22 ++++++++++ .../smallbasic/pc/LoggingInputStream.java | 40 +++++++++++++++++++ .../smallbasic/pc/LoggingOutputStream.java | 30 ++++++++++++++ .../pc/SerialPortIOIOConnection.java | 4 +- ioio/main.cpp | 4 +- ioio/samples/led.bas | 14 +++---- 7 files changed, 104 insertions(+), 12 deletions(-) create mode 100644 ioio/ioio/src/main/java/ioio/smallbasic/pc/HexDump.java create mode 100644 ioio/ioio/src/main/java/ioio/smallbasic/pc/LoggingInputStream.java create mode 100644 ioio/ioio/src/main/java/ioio/smallbasic/pc/LoggingOutputStream.java diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/IOIOImpl.java b/ioio/ioio/src/main/java/ioio/smallbasic/IOIOImpl.java index 0f3d455..6f4bf9e 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/IOIOImpl.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/IOIOImpl.java @@ -59,7 +59,7 @@ public void sync() { lock.invoke(IOIO::sync); } - public void waitForConnect(int latency) { + public void waitForConnect() { IOUtil.setError(null); IOService.getInstance().start(); handleError(); diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/pc/HexDump.java b/ioio/ioio/src/main/java/ioio/smallbasic/pc/HexDump.java new file mode 100644 index 0000000..99a1747 --- /dev/null +++ b/ioio/ioio/src/main/java/ioio/smallbasic/pc/HexDump.java @@ -0,0 +1,22 @@ +package ioio.smallbasic.pc; + +public class HexDump { + private int index; + private final String prefix; + + HexDump(String prefix) { + this.index = 0; + this.prefix = prefix; + } + + void print(int data) { + if (index % 16 == 0) { + if (index != 0) { + System.out.println(); + } + System.out.printf(" %08x ", index); + } + System.out.printf("%s:%02x ", prefix, data); + index++; + } +} diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/pc/LoggingInputStream.java b/ioio/ioio/src/main/java/ioio/smallbasic/pc/LoggingInputStream.java new file mode 100644 index 0000000..037fecf --- /dev/null +++ b/ioio/ioio/src/main/java/ioio/smallbasic/pc/LoggingInputStream.java @@ -0,0 +1,40 @@ +package ioio.smallbasic.pc; + +import java.io.IOException; +import java.io.InputStream; + +public class LoggingInputStream extends InputStream { + private static final int DEBUG_OUT = 0x24; + private final InputStream wrappedStream; + private final HexDump hexDump; + + public LoggingInputStream(InputStream wrappedStream) { + this.wrappedStream = wrappedStream; + this.hexDump = new HexDump("RX"); + } + + @Override + public int read() throws IOException { + // see: IOIOLibCore/src/main/java/ioio/lib/impl/IOIOProtocol.java + int data = wrappedStream.read(); + if (data == DEBUG_OUT) { + System.out.print("[FIRMWARE] "); + data = wrappedStream.read(); + while (data != '\n') { + System.out.print(Character.valueOf((char) data)); + data = wrappedStream.read(); + } + System.out.println(); + data = read(); + } else { + hexDump.print(data); + } + return data; + } + + @Override + public void close() throws IOException { + wrappedStream.close(); + } +} + diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/pc/LoggingOutputStream.java b/ioio/ioio/src/main/java/ioio/smallbasic/pc/LoggingOutputStream.java new file mode 100644 index 0000000..f040068 --- /dev/null +++ b/ioio/ioio/src/main/java/ioio/smallbasic/pc/LoggingOutputStream.java @@ -0,0 +1,30 @@ +package ioio.smallbasic.pc; + +import java.io.IOException; +import java.io.OutputStream; + +public class LoggingOutputStream extends OutputStream { + private final OutputStream wrappedStream; + private final HexDump hexDump; + + public LoggingOutputStream(OutputStream outputStream) { + this.wrappedStream = outputStream; + this.hexDump = new HexDump("TX"); + } + + @Override + public void write(int b) throws IOException { + wrappedStream.write(b); + hexDump.print(b); + } + + @Override + public void flush() throws IOException { + wrappedStream.flush(); + } + + @Override + public void close() throws IOException { + wrappedStream.close(); + } +} diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/pc/SerialPortIOIOConnection.java b/ioio/ioio/src/main/java/ioio/smallbasic/pc/SerialPortIOIOConnection.java index eb1163c..fcc519f 100644 --- a/ioio/ioio/src/main/java/ioio/smallbasic/pc/SerialPortIOIOConnection.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/pc/SerialPortIOIOConnection.java @@ -62,8 +62,8 @@ public OutputStream getOutputStream() throws ConnectionLostException { @Override public void waitForConnect() throws ConnectionLostException { if (!abort && serialPort.openPort()) { - inputStream = serialPort.getInputStream(); - outputStream = serialPort.getOutputStream(); + inputStream = new LoggingInputStream(serialPort.getInputStream()); + outputStream = new LoggingOutputStream(serialPort.getOutputStream()); serialPort.setDTR(); } else { throw new ConnectionLostException(); diff --git a/ioio/main.cpp b/ioio/main.cpp index d8852c5..8187701 100644 --- a/ioio/main.cpp +++ b/ioio/main.cpp @@ -244,7 +244,7 @@ FUNC_SIG lib_proc[] = { {0, 0, "HARDRESET", cmd_ioio_hardreset}, {0, 0, "SOFTRESET", cmd_ioio_softreset}, {0, 0, "SYNC", cmd_ioio_sync}, - {1, 1, "WAITFORCONNECT", cmd_ioio_waitforconnect}, + {0, 0, "WAITFORCONNECT", cmd_ioio_waitforconnect}, {0, 0, "WAITFORDISCONNECT", cmd_ioio_waitfordisconnect}, }; @@ -261,7 +261,7 @@ SBLIB_API int sblib_func_count() { // int sblib_init(const char *sourceFile) { #if defined(DESKTOP_MODULE) - int result = createJVM("-Djava.class.path=./ioio-1.0-jar-with-dependencies.jar", "-Dioio.SerialPorts=IOIO0", false); + int result = createJVM("-Djava.class.path=./ioio-1.0-jar-with-dependencies.jar", "-Dioio.SerialPorts=ttyACM0", false); #else int result = 1; #endif diff --git a/ioio/samples/led.bas b/ioio/samples/led.bas index 955340a..06c1f0b 100644 --- a/ioio/samples/led.bas +++ b/ioio/samples/led.bas @@ -1,22 +1,22 @@ import ioio -out = ioio.openDigitalOutput(0) +out = ioio.openDigitalOutput(13) -print "wait for connect" -ioio.waitForConnect(10) -print "ready!!!" +'print "wait for connect" +ioio.waitForConnect() +'print "ready!!!" value = false for i = 0 to 5 out.write(value) value = !value - delay 1000 + delay 2000 next for i = 0 to 5 out.write(value) value = !value - delay 100 + delay 3000 next -print "done" +'print "done" From 498c61061c1670ada7fc2c69946e7136ccc86ad5 Mon Sep 17 00:00:00 2001 From: chrisws Date: Thu, 31 Oct 2024 21:09:54 +1030 Subject: [PATCH 057/131] Update dependencies --- gtk-server/uthash | 2 +- ioio/README.md | 2 +- ioio/api.json | 2 +- nuklear/Nuklear | 2 +- nuklear/main.cpp | 2 +- raylib/README.md | 78 ++++++++++++++-------------- raylib/func-def.h | 24 +++++---- raylib/func.h | 125 ++++++++++++++++++++++++++------------------- raylib/proc-def.h | 2 +- raylib/proc.h | 37 +++++++------- raylib/raygui | 2 +- raylib/raylib | 2 +- websocket/mongoose | 2 +- 13 files changed, 154 insertions(+), 128 deletions(-) diff --git a/gtk-server/uthash b/gtk-server/uthash index 619fe95..f69112c 160000 --- a/gtk-server/uthash +++ b/gtk-server/uthash @@ -1 +1 @@ -Subproject commit 619fe95ca4679249528f086536aadd0c469dbd99 +Subproject commit f69112c04f1b6e059b8071cb391a1fcc83791a00 diff --git a/ioio/README.md b/ioio/README.md index cc4463e..b70c2b9 100644 --- a/ioio/README.md +++ b/ioio/README.md @@ -14,7 +14,7 @@ This interface provides control over all the IOIO board functions. |void hardReset(void)|Equivalent to disconnecting and reconnecting the board power supply.| |void softReset(void)|Resets the entire state (returning to initial state), without dropping the connection.| |void sync(void)|Sends a message to the IOIO and waits for an echo.| -|void waitForConnect(int)|Establishes connection with the IOIO board.| +|void waitForConnect(void)|Establishes connection with the IOIO board.| |void waitForDisconnect(void)|Blocks until IOIO has been disconnected and all connection-related resources have been freed, so that a new connection can be attempted.| ## AnalogInput diff --git a/ioio/api.json b/ioio/api.json index 66b53e6..c3bc888 100644 --- a/ioio/api.json +++ b/ioio/api.json @@ -36,7 +36,7 @@ },{ "name": "waitForConnect" "rtn": "void", - "arg": "int", + "arg": "void", "comment": "Establishes connection with the IOIO board." },{ "name": "waitForDisconnect" diff --git a/nuklear/Nuklear b/nuklear/Nuklear index ca8aaf3..6566d90 160000 --- a/nuklear/Nuklear +++ b/nuklear/Nuklear @@ -1 +1 @@ -Subproject commit ca8aaf3f65e9fc8ac5d90fefc1204585add4b89b +Subproject commit 6566d9075d5fed48af014c93f87c4aed8c4bd21c diff --git a/nuklear/main.cpp b/nuklear/main.cpp index 6a64e09..00ac348 100644 --- a/nuklear/main.cpp +++ b/nuklear/main.cpp @@ -224,7 +224,7 @@ static void get_rgba_str(struct nk_color c, char *str) { } static int get_value(const char *str, int range) { - const char *end; + char *end; int result = nk_strtoi(str, &end); if (*end == '%') { result = result * range / 100; diff --git a/raylib/README.md b/raylib/README.md index 2f30f4f..bb15020 100644 --- a/raylib/README.md +++ b/raylib/README.md @@ -1,10 +1,10 @@ -*Raylib* _MAJOR 5 _MINOR 5 _PATCH 0 5.5-dev +*Raylib* _MAJOR 5 _MINOR 5 _PATCH 0 5.5 ======= raylib is a simple and easy-to-use library to enjoy videogames programming. https://www.raylib.com/ -Implemented APIs (629) +Implemented APIs (631) ---------------- | Name | Description | @@ -49,6 +49,9 @@ Implemented APIs (629) | func ColorToHSV(color) | Get HSV values for a Color, hue [0..360], saturation/value [0..1] | | func ColorToInt(color) | Get hexadecimal value for a Color (0xRRGGBBAA) | | func CompressData(data, dataSize, compDataSize) | Compress data (DEFLATE algorithm), memory must be MemFree() | +| func ComputeCRC32(data, dataSize) | Compute CRC32 hash code | +| func ComputeMD5(data, dataSize) | Compute MD5 hash code, returns static int[4] (16 bytes) | +| func ComputeSHA1(data, dataSize) | Compute SHA1 hash code, returns static int[5] (20 bytes) | | func createPhysicsbodycircle() | n/a | | func createPhysicsbodypolygon() | n/a | | func createPhysicsbodyrectangle() | n/a | @@ -96,8 +99,8 @@ Implemented APIs (629) | sub DrawModelPointsEx(model, position, rotationAxis, rotationAngle, scale, tint) | Draw a model as points with extended parameters | | sub DrawModelWires(model, position, scale, tint) | Draw a model wires (with texture if set) | | sub DrawModelWiresEx(model, position, rotationAxis, rotationAngle, scale, tint) | Draw a model wires (with texture if set) with extended parameters | -| sub DrawPixel(posX, posY, color) | Draw a pixel | -| sub DrawPixelV(position, color) | Draw a pixel (Vector version) | +| sub DrawPixel(posX, posY, color) | Draw a pixel using geometry [Can be slow, use with care] | +| sub DrawPixelV(position, color) | Draw a pixel using geometry (Vector version) [Can be slow, use with care] | | sub DrawPlane(centerPos, size, color) | Draw a plane XZ | | sub DrawPoint3D(position, color) | Draw a point in 3D space, actually a small line | | sub DrawPoly(center, sides, radius, rotation, color) | Draw a regular polygon (Vector version) | @@ -204,7 +207,7 @@ Implemented APIs (629) | func GetCodepointPrevious(text, codepointSize) | Get previous codepoint in a UTF-8 encoded string, 0x3f('?') is returned on failure | | func GetCollisionRec(rec1, rec2) | Get collision rectangle for two rectangles collision | | func GetColor(hexValue) | Get Color structure from hexadecimal value | -| func GetCurrentMonitor() | Get current connected monitor | +| func GetCurrentMonitor() | Get current monitor where window is placed | | func GetDirectoryPath(filePath) | Get full path for a given fileName with path (uses static string) | | func GetFileExtension(fileName) | Get pointer to extension for a filename string (includes dot: '.png') | | func GetFileLength(fileName) | Get file length in bytes (NOTE: GetFileSize() conflicts with windows.h) | @@ -221,7 +224,7 @@ Implemented APIs (629) | func GetGestureDetected() | Get latest detected gesture | | func GetGestureDragAngle() | Get gesture drag angle | | func GetGestureDragVector() | Get gesture drag vector | -| func GetGestureHoldDuration() | Get gesture hold time in milliseconds | +| func GetGestureHoldDuration() | Get gesture hold time in seconds | | func GetGesturePinchAngle() | Get gesture pinch angle | | func GetGesturePinchVector() | Get gesture pinch delta | | func GetGlyphAtlasRec(font, codepoint) | Get glyph rectangle in font atlas for a codepoint (unicode character), fallback to '?' if not found | @@ -387,45 +390,45 @@ Implemented APIs (629) | func IsAudioDeviceReady() | Check if audio device has been initialized successfully | | func IsAudioStreamPlaying(stream) | Check if audio stream is playing | | func IsAudioStreamProcessed(stream) | Check if any audio stream buffers requires refill | -| func IsAudioStreamReady(stream) | Checks if an audio stream is ready | +| func IsAudioStreamValid(stream) | Checks if an audio stream is valid (buffers initialized) | | func IsCursorHidden() | Check if cursor is not visible | | func IsCursorOnScreen() | Check if cursor is on the screen | | func IsFileDropped() | Check if a file has been dropped into window | | func IsFileExtension(fileName, ext) | Check file extension (including point: .png, .wav) | | func IsFileNameValid(fileName) | Check if fileName is valid for the platform/OS | -| func IsFontReady(font) | Check if a font is ready | +| func IsFontValid(font) | Check if a font is valid (font data loaded, WARNING: GPU texture not checked) | | func IsGamepadAvailable(gamepad) | Check if a gamepad is available | | func IsGamepadButtonDown(gamepad, button) | Check if a gamepad button is being pressed | | func IsGamepadButtonPressed(gamepad, button) | Check if a gamepad button has been pressed once | | func IsGamepadButtonReleased(gamepad, button) | Check if a gamepad button has been released once | | func IsGamepadButtonUp(gamepad, button) | Check if a gamepad button is NOT being pressed | | func IsGestureDetected(gesture) | Check if a gesture have been detected | -| func IsImageReady(image) | Check if an image is ready | +| func IsImageValid(image) | Check if an image is valid (data and parameters) | | func IsKeyDown(key) | Check if a key is being pressed | | func IsKeyPressed(key) | Check if a key has been pressed once | -| func IsKeyPressedRepeat(key) | Check if a key has been pressed again (Only PLATFORM_DESKTOP) | +| func IsKeyPressedRepeat(key) | Check if a key has been pressed again | | func IsKeyReleased(key) | Check if a key has been released once | | func IsKeyUp(key) | Check if a key is NOT being pressed | | func IsModelAnimationValid(model, anim) | Check model animation skeleton match | -| func IsModelReady(model) | Check if a model is ready | +| func IsModelValid(model) | Check if a model is valid (loaded in GPU, VAO/VBOs) | | func IsMouseButtonDown(button) | Check if a mouse button is being pressed | | func IsMouseButtonPressed(button) | Check if a mouse button has been pressed once | | func IsMouseButtonReleased(button) | Check if a mouse button has been released once | | func IsMouseButtonUp(button) | Check if a mouse button is NOT being pressed | -| func IsMusicReady(music) | Checks if a music stream is ready | | func IsMusicStreamPlaying(music) | Check if music is playing | +| func IsMusicValid(music) | Checks if a music stream is valid (context and buffers initialized) | | func IsPathFile(path) | Check if a given path is a file or a directory | -| func IsRenderTextureReady(target) | Check if a render texture is ready | -| func IsShaderReady(shader) | Check if a shader is ready | +| func IsRenderTextureValid(target) | Check if a render texture is valid (loaded in GPU) | +| func IsShaderValid(shader) | Check if a shader is valid (loaded on GPU) | | func IsSoundPlaying(sound) | Check if a sound is currently playing | -| func IsSoundReady(sound) | Checks if a sound is ready | -| func IsTextureReady(texture) | Check if a texture is ready | -| func IsWaveReady(wave) | Checks if wave data is ready | -| func IsWindowFocused() | Check if window is currently focused (only PLATFORM_DESKTOP) | +| func IsSoundValid(sound) | Checks if a sound is valid (data loaded and buffers initialized) | +| func IsTextureValid(texture) | Check if a texture is valid (loaded in GPU) | +| func IsWaveValid(wave) | Checks if wave data is valid (data loaded and parameters) | +| func IsWindowFocused() | Check if window is currently focused | | func IsWindowFullscreen() | Check if window is currently fullscreen | -| func IsWindowHidden() | Check if window is currently hidden (only PLATFORM_DESKTOP) | -| func IsWindowMaximized() | Check if window is currently maximized (only PLATFORM_DESKTOP) | -| func IsWindowMinimized() | Check if window is currently minimized (only PLATFORM_DESKTOP) | +| func IsWindowHidden() | Check if window is currently hidden | +| func IsWindowMaximized() | Check if window is currently maximized | +| func IsWindowMinimized() | Check if window is currently minimized | | func IsWindowReady() | Check if window has been initialized successfully | | func IsWindowResized() | Check if window has been resized last frame | | func IsWindowState(flag) | Check if one specific window flag is enabled | @@ -450,7 +453,6 @@ Implemented APIs (629) | func LoadImageFromTexture(texture) | Load image from GPU texture data | | func LoadImagePalette(image, maxPaletteSize, colorCount) | Load colors palette from image as a Color array (RGBA - 32bit) | | func LoadImageRaw(fileName, width, height, format, headerSize) | Load image from RAW file data | -| func LoadImageSvg(fileNameOrString, width, height) | Load image from SVG file data or string with specified size | | func LoadModel(fileName) | Load model from files (meshes and materials) | | func LoadModelAnimations(fileName, animCount) | Load model animations from file | | func LoadModelFromMesh(mesh) | Load model from generated mesh (default material) | @@ -471,14 +473,14 @@ Implemented APIs (629) | func LoadWaveFromMemory(fileType, fileData, dataSize) | Load wave from memory buffer, fileType refers to extension: i.e. '.wav' | | func LoadWaveSamples(wave) | Load samples data from wave as a 32bit float data array | | func MakeDirectory(dirPath) | Create directories (including full path requested), returns 0 on success | -| sub MaximizeWindow() | Set window state: maximized, if resizable (only PLATFORM_DESKTOP) | +| sub MaximizeWindow() | Set window state: maximized, if resizable | | func MeasureText(text, fontSize) | Measure string width for default font | | func MeasureTextEx(font, text, fontSize, spacing) | Measure string size for Font | | func MemAlloc(size) | Internal memory allocator | | sub MemFree(ptr) | Internal memory free | | func MemRealloc(ptr, size) | Internal memory reallocator | | func meshboundingbox() | n/a | -| sub MinimizeWindow() | Set window state: minimized, if resizable (only PLATFORM_DESKTOP) | +| sub MinimizeWindow() | Set window state: minimized, if resizable | | sub OpenURL(url) | Open URL with default system browser (if available) | | sub PauseAudioStream(stream) | Pause audio stream | | sub PauseMusicStream(music) | Pause music playing | @@ -494,7 +496,7 @@ Implemented APIs (629) | func pollevents() | n/a | | sub PollInputEvents() | Register all input events | | func resetPhysics() | n/a | -| sub RestoreWindow() | Set window state: not minimized/maximized (only PLATFORM_DESKTOP) | +| sub RestoreWindow() | Set window state: not minimized/maximized | | sub ResumeAudioStream(stream) | Resume audio stream | | sub ResumeMusicStream(music) | Resume playing paused music | | sub ResumeSound(sound) | Resume a paused sound | @@ -511,7 +513,7 @@ Implemented APIs (629) | sub SetConfigFlags(flags) | Setup init configuration flags (view FLAGS) | | sub SetExitKey(key) | Set a custom key to exit program (default is ESC) | | func SetGamepadMappings(mappings) | Set internal gamepad mappings (SDL_GameControllerDB) | -| sub SetGamepadVibration(gamepad, leftMotor, rightMotor) | Set gamepad vibration for both motors | +| sub SetGamepadVibration(gamepad, leftMotor, rightMotor, duration) | Set gamepad vibration for both motors (duration in seconds) | | sub SetGesturesEnabled(flags) | Enable a set of gestures using flags | | sub SetMasterVolume(volume) | Set master volume (listener) | | func setmodeldiffusetexture() | n/a | @@ -559,17 +561,17 @@ Implemented APIs (629) | sub SetTextureFilter(texture, filter) | Set texture scaling filter mode | | sub SetTextureWrap(texture, wrap) | Set texture wrapping mode | | sub SetTraceLogLevel(logLevel) | Set the current threshold (minimum) log level | -| sub SetWindowFocused() | Set window focused (only PLATFORM_DESKTOP) | -| sub SetWindowIcon(image) | Set icon for window (single image, RGBA 32bit, only PLATFORM_DESKTOP) | -| sub SetWindowIcons(images, count) | Set icon for window (multiple images, RGBA 32bit, only PLATFORM_DESKTOP) | +| sub SetWindowFocused() | Set window focused | +| sub SetWindowIcon(image) | Set icon for window (single image, RGBA 32bit) | +| sub SetWindowIcons(images, count) | Set icon for window (multiple images, RGBA 32bit) | | sub SetWindowMaxSize(width, height) | Set window maximum dimensions (for FLAG_WINDOW_RESIZABLE) | | sub SetWindowMinSize(width, height) | Set window minimum dimensions (for FLAG_WINDOW_RESIZABLE) | | sub SetWindowMonitor(monitor) | Set monitor for the current window | -| sub SetWindowOpacity(opacity) | Set window opacity [0.0f..1.0f] (only PLATFORM_DESKTOP) | -| sub SetWindowPosition(x, y) | Set window position on screen (only PLATFORM_DESKTOP) | +| sub SetWindowOpacity(opacity) | Set window opacity [0.0f..1.0f] | +| sub SetWindowPosition(x, y) | Set window position on screen | | sub SetWindowSize(width, height) | Set window dimensions | -| sub SetWindowState(flags) | Set window configuration state using flags (only PLATFORM_DESKTOP) | -| sub SetWindowTitle(title) | Set title for window (only PLATFORM_DESKTOP and PLATFORM_WEB) | +| sub SetWindowState(flags) | Set window configuration state using flags | +| sub SetWindowTitle(title) | Set title for window | | sub ShowCursor() | Shows cursor | | sub StartAutomationEventRecording() | Start recording automation events (AutomationEventList must be set) | | sub StopAudioStream(stream) | Stop audio stream | @@ -594,8 +596,8 @@ Implemented APIs (629) | func TextToPascal(text) | Get Pascal case notation version of provided string | | func TextToSnake(text) | Get Snake case notation version of provided string | | func TextToUpper(text) | Get upper case version of provided string | -| sub ToggleBorderlessWindowed() | Toggle window state: borderless windowed [resizes window to match monitor resolution] (only PLATFORM_DESKTOP) | -| sub ToggleFullscreen() | Toggle window state: fullscreen/windowed [resizes monitor to match window resolution] (only PLATFORM_DESKTOP) | +| sub ToggleBorderlessWindowed() | Toggle window state: borderless windowed, resizes window to match monitor resolution | +| sub ToggleFullscreen() | Toggle window state: fullscreen/windowed, resizes monitor to match window resolution | | sub UnloadAudioStream(stream) | Unload audio stream and free memory | | sub UnloadAutomationEventList(list) | Unload automation events list from file | | sub UnloadCodepoints(codepoints) | Unload codepoints data from memory | @@ -624,8 +626,8 @@ Implemented APIs (629) | func updateautomationeventlist() | n/a | | sub UpdateCamera(camera, mode) | Update camera position for selected mode | | sub UpdateMeshBuffer(mesh, index, data, dataSize, offset) | Update mesh vertex data in GPU for a specific buffer index | -| sub UpdateModelAnimation(model, anim, frame) | Update model animation pose | -| sub UpdateModelAnimationBoneMatrices(model, anim, frame) | Update model animation mesh bone matrices | +| sub UpdateModelAnimation(model, anim, frame) | Update model animation pose (CPU) | +| sub UpdateModelAnimationBoneMatrices(model, anim, frame) | Update model animation mesh bone matrices (GPU skinning) | | sub UpdateMusicStream(music) | Updates buffers for music streaming | | func updatePhysics() | n/a | | sub UpdateSound(sound, data, sampleCount) | Update sound buffer with new data | @@ -653,7 +655,7 @@ Unimplemented APIs | DrawMeshInstanced | Draw multiple mesh instances with material and different transforms | | GenImageFontAtlas | Generate image font atlas using chars info | | GetGlyphInfo | Get glyph font info data for a codepoint (unicode character), fallback to '?' if not found | -| IsMaterialReady | Check if a material is ready | +| IsMaterialValid | Check if a material is valid (shader assigned, map textures loaded in GPU) | | LoadFontData | Load font data for further use | | LoadMaterialDefault | Load default material (Supports: DIFFUSE, SPECULAR, NORMAL maps) | | LoadMaterials | Load materials from model file | diff --git a/raylib/func-def.h b/raylib/func-def.h index a3443cb..9377efb 100644 --- a/raylib/func-def.h +++ b/raylib/func-def.h @@ -26,6 +26,9 @@ {1, 1, "COLORTOHSV", cmd_colortohsv}, {1, 1, "COLORTOINT", cmd_colortoint}, {2, 2, "COMPRESSDATA", cmd_compressdata}, + {2, 2, "COMPUTECRC32", cmd_computecrc32}, + {2, 2, "COMPUTEMD5", cmd_computemd5}, + {2, 2, "COMPUTESHA1", cmd_computesha1}, {1, 1, "DECODEDATABASE64", cmd_decodedatabase64}, {2, 2, "DECOMPRESSDATA", cmd_decompressdata}, {1, 1, "DIRECTORYEXISTS", cmd_directoryexists}, @@ -162,40 +165,40 @@ {0, 0, "ISAUDIODEVICEREADY", cmd_isaudiodeviceready}, {1, 1, "ISAUDIOSTREAMPLAYING", cmd_isaudiostreamplaying}, {1, 1, "ISAUDIOSTREAMPROCESSED", cmd_isaudiostreamprocessed}, - {1, 1, "ISAUDIOSTREAMREADY", cmd_isaudiostreamready}, + {1, 1, "ISAUDIOSTREAMVALID", cmd_isaudiostreamvalid}, {0, 0, "ISCURSORHIDDEN", cmd_iscursorhidden}, {0, 0, "ISCURSORONSCREEN", cmd_iscursoronscreen}, {0, 0, "ISFILEDROPPED", cmd_isfiledropped}, {2, 2, "ISFILEEXTENSION", cmd_isfileextension}, {1, 1, "ISFILENAMEVALID", cmd_isfilenamevalid}, - {1, 1, "ISFONTREADY", cmd_isfontready}, + {1, 1, "ISFONTVALID", cmd_isfontvalid}, {1, 1, "ISGAMEPADAVAILABLE", cmd_isgamepadavailable}, {2, 2, "ISGAMEPADBUTTONDOWN", cmd_isgamepadbuttondown}, {2, 2, "ISGAMEPADBUTTONPRESSED", cmd_isgamepadbuttonpressed}, {2, 2, "ISGAMEPADBUTTONRELEASED", cmd_isgamepadbuttonreleased}, {2, 2, "ISGAMEPADBUTTONUP", cmd_isgamepadbuttonup}, {1, 1, "ISGESTUREDETECTED", cmd_isgesturedetected}, - {1, 1, "ISIMAGEREADY", cmd_isimageready}, + {1, 1, "ISIMAGEVALID", cmd_isimagevalid}, {1, 1, "ISKEYDOWN", cmd_iskeydown}, {1, 1, "ISKEYPRESSED", cmd_iskeypressed}, {1, 1, "ISKEYPRESSEDREPEAT", cmd_iskeypressedrepeat}, {1, 1, "ISKEYRELEASED", cmd_iskeyreleased}, {1, 1, "ISKEYUP", cmd_iskeyup}, {2, 2, "ISMODELANIMATIONVALID", cmd_ismodelanimationvalid}, - {1, 1, "ISMODELREADY", cmd_ismodelready}, + {1, 1, "ISMODELVALID", cmd_ismodelvalid}, {1, 1, "ISMOUSEBUTTONDOWN", cmd_ismousebuttondown}, {1, 1, "ISMOUSEBUTTONPRESSED", cmd_ismousebuttonpressed}, {1, 1, "ISMOUSEBUTTONRELEASED", cmd_ismousebuttonreleased}, {1, 1, "ISMOUSEBUTTONUP", cmd_ismousebuttonup}, - {1, 1, "ISMUSICREADY", cmd_ismusicready}, {1, 1, "ISMUSICSTREAMPLAYING", cmd_ismusicstreamplaying}, + {1, 1, "ISMUSICVALID", cmd_ismusicvalid}, {1, 1, "ISPATHFILE", cmd_ispathfile}, - {1, 1, "ISRENDERTEXTUREREADY", cmd_isrendertextureready}, - {1, 1, "ISSHADERREADY", cmd_isshaderready}, + {1, 1, "ISRENDERTEXTUREVALID", cmd_isrendertexturevalid}, + {1, 1, "ISSHADERVALID", cmd_isshadervalid}, {1, 1, "ISSOUNDPLAYING", cmd_issoundplaying}, - {1, 1, "ISSOUNDREADY", cmd_issoundready}, - {1, 1, "ISTEXTUREREADY", cmd_istextureready}, - {1, 1, "ISWAVEREADY", cmd_iswaveready}, + {1, 1, "ISSOUNDVALID", cmd_issoundvalid}, + {1, 1, "ISTEXTUREVALID", cmd_istexturevalid}, + {1, 1, "ISWAVEVALID", cmd_iswavevalid}, {0, 0, "ISWINDOWFOCUSED", cmd_iswindowfocused}, {0, 0, "ISWINDOWFULLSCREEN", cmd_iswindowfullscreen}, {0, 0, "ISWINDOWHIDDEN", cmd_iswindowhidden}, @@ -225,7 +228,6 @@ {1, 1, "LOADIMAGEFROMTEXTURE", cmd_loadimagefromtexture}, {2, 2, "LOADIMAGEPALETTE", cmd_loadimagepalette}, {5, 5, "LOADIMAGERAW", cmd_loadimageraw}, - {3, 3, "LOADIMAGESVG", cmd_loadimagesvg}, {1, 1, "LOADMODEL", cmd_loadmodel}, {1, 1, "LOADMODELFROMMESH", cmd_loadmodelfrommesh}, {1, 1, "LOADMUSICSTREAM", cmd_loadmusicstream}, diff --git a/raylib/func.h b/raylib/func.h index 45d69af..51912f8 100644 --- a/raylib/func.h +++ b/raylib/func.h @@ -324,6 +324,39 @@ static int cmd_compressdata(int argc, slib_par_t *params, var_t *retval) { return 1; } +// +// Compute CRC32 hash code +// +static int cmd_computecrc32(int argc, slib_par_t *params, var_t *retval) { + auto data = (unsigned char *)get_param_str(argc, params, 0, 0); + auto dataSize = get_param_int(argc, params, 1, 0); + auto fnResult = ComputeCRC32(data, dataSize); + v_setint(retval, fnResult); + return 1; +} + +// +// Compute MD5 hash code, returns static int[4] (16 bytes) +// +static int cmd_computemd5(int argc, slib_par_t *params, var_t *retval) { + auto data = (unsigned char *)get_param_str(argc, params, 0, 0); + auto dataSize = get_param_int(argc, params, 1, 0); + auto fnResult = (var_int_t)ComputeMD5(data, dataSize); + v_setint(retval, fnResult); + return 1; +} + +// +// Compute SHA1 hash code, returns static int[5] (20 bytes) +// +static int cmd_computesha1(int argc, slib_par_t *params, var_t *retval) { + auto data = (unsigned char *)get_param_str(argc, params, 0, 0); + auto dataSize = get_param_int(argc, params, 1, 0); + auto fnResult = (var_int_t)ComputeSHA1(data, dataSize); + v_setint(retval, fnResult); + return 1; +} + // // Decode Base64 string data, memory must be MemFree() // @@ -935,7 +968,7 @@ static int cmd_getcolor(int argc, slib_par_t *params, var_t *retval) { } // -// Get current connected monitor +// Get current monitor where window is placed // static int cmd_getcurrentmonitor(int argc, slib_par_t *params, var_t *retval) { auto fnResult = GetCurrentMonitor(); @@ -1098,7 +1131,7 @@ static int cmd_getgesturedragvector(int argc, slib_par_t *params, var_t *retval) } // -// Get gesture hold time in milliseconds +// Get gesture hold time in seconds // static int cmd_getgestureholdduration(int argc, slib_par_t *params, var_t *retval) { auto fnResult = GetGestureHoldDuration(); @@ -1933,13 +1966,13 @@ static int cmd_isaudiostreamprocessed(int argc, slib_par_t *params, var_t *retva } // -// Checks if an audio stream is ready +// Checks if an audio stream is valid (buffers initialized) // -static int cmd_isaudiostreamready(int argc, slib_par_t *params, var_t *retval) { +static int cmd_isaudiostreamvalid(int argc, slib_par_t *params, var_t *retval) { int result; int stream_id = get_audiostream_id(argc, params, 0, retval); if (stream_id != -1) { - auto fnResult = IsAudioStreamReady(_audioStream.at(stream_id)); + auto fnResult = IsAudioStreamValid(_audioStream.at(stream_id)); v_setint(retval, fnResult); result = 1; } else { @@ -1997,13 +2030,13 @@ static int cmd_isfilenamevalid(int argc, slib_par_t *params, var_t *retval) { } // -// Check if a font is ready +// Check if a font is valid (font data loaded, WARNING: GPU texture not checked) // -static int cmd_isfontready(int argc, slib_par_t *params, var_t *retval) { +static int cmd_isfontvalid(int argc, slib_par_t *params, var_t *retval) { int result; int font_id = get_font_id(argc, params, 0, retval); if (font_id != -1) { - auto fnResult = IsFontReady(_fontMap.at(font_id)); + auto fnResult = IsFontValid(_fontMap.at(font_id)); v_setint(retval, fnResult); result = 1; } else { @@ -2077,13 +2110,13 @@ static int cmd_isgesturedetected(int argc, slib_par_t *params, var_t *retval) { } // -// Check if an image is ready +// Check if an image is valid (data and parameters) // -static int cmd_isimageready(int argc, slib_par_t *params, var_t *retval) { +static int cmd_isimagevalid(int argc, slib_par_t *params, var_t *retval) { int result; int image_id = get_image_id(argc, params, 0, retval); if (image_id != -1) { - auto fnResult = IsImageReady(_imageMap.at(image_id)); + auto fnResult = IsImageValid(_imageMap.at(image_id)); v_setint(retval, fnResult); result = 1; } else { @@ -2113,7 +2146,7 @@ static int cmd_iskeypressed(int argc, slib_par_t *params, var_t *retval) { } // -// Check if a key has been pressed again (Only PLATFORM_DESKTOP) +// Check if a key has been pressed again // static int cmd_iskeypressedrepeat(int argc, slib_par_t *params, var_t *retval) { auto key = get_param_int(argc, params, 0, 0); @@ -2160,13 +2193,13 @@ static int cmd_ismodelanimationvalid(int argc, slib_par_t *params, var_t *retval } // -// Check if a model is ready +// Check if a model is valid (loaded in GPU, VAO/VBOs) // -static int cmd_ismodelready(int argc, slib_par_t *params, var_t *retval) { +static int cmd_ismodelvalid(int argc, slib_par_t *params, var_t *retval) { int result; int model_id = get_model_id(argc, params, 0, retval); if (model_id != -1) { - auto fnResult = IsModelReady(_modelMap.at(model_id)); + auto fnResult = IsModelValid(_modelMap.at(model_id)); v_setint(retval, fnResult); result = 1; } else { @@ -2216,13 +2249,13 @@ static int cmd_ismousebuttonup(int argc, slib_par_t *params, var_t *retval) { } // -// Checks if a music stream is ready +// Check if music is playing // -static int cmd_ismusicready(int argc, slib_par_t *params, var_t *retval) { +static int cmd_ismusicstreamplaying(int argc, slib_par_t *params, var_t *retval) { int result; int music_id = get_music_id(argc, params, 0, retval); if (music_id != -1) { - auto fnResult = IsMusicReady(_musicMap.at(music_id)); + auto fnResult = IsMusicStreamPlaying(_musicMap.at(music_id)); v_setint(retval, fnResult); result = 1; } else { @@ -2232,13 +2265,13 @@ static int cmd_ismusicready(int argc, slib_par_t *params, var_t *retval) { } // -// Check if music is playing +// Checks if a music stream is valid (context and buffers initialized) // -static int cmd_ismusicstreamplaying(int argc, slib_par_t *params, var_t *retval) { +static int cmd_ismusicvalid(int argc, slib_par_t *params, var_t *retval) { int result; int music_id = get_music_id(argc, params, 0, retval); if (music_id != -1) { - auto fnResult = IsMusicStreamPlaying(_musicMap.at(music_id)); + auto fnResult = IsMusicValid(_musicMap.at(music_id)); v_setint(retval, fnResult); result = 1; } else { @@ -2258,13 +2291,13 @@ static int cmd_ispathfile(int argc, slib_par_t *params, var_t *retval) { } // -// Check if a render texture is ready +// Check if a render texture is valid (loaded in GPU) // -static int cmd_isrendertextureready(int argc, slib_par_t *params, var_t *retval) { +static int cmd_isrendertexturevalid(int argc, slib_par_t *params, var_t *retval) { int result; int target_id = get_render_texture_id(argc, params, 0, retval); if (target_id != -1) { - auto fnResult = IsRenderTextureReady(_renderMap.at(target_id)); + auto fnResult = IsRenderTextureValid(_renderMap.at(target_id)); v_setint(retval, fnResult); result = 1; } else { @@ -2274,11 +2307,11 @@ static int cmd_isrendertextureready(int argc, slib_par_t *params, var_t *retval) } // -// Check if a shader is ready +// Check if a shader is valid (loaded on GPU) // -static int cmd_isshaderready(int argc, slib_par_t *params, var_t *retval) { +static int cmd_isshadervalid(int argc, slib_par_t *params, var_t *retval) { auto shader = get_param_shader(argc, params, 0); - auto fnResult = IsShaderReady(shader); + auto fnResult = IsShaderValid(shader); v_setint(retval, fnResult); return 1; } @@ -2300,13 +2333,13 @@ static int cmd_issoundplaying(int argc, slib_par_t *params, var_t *retval) { } // -// Checks if a sound is ready +// Checks if a sound is valid (data loaded and buffers initialized) // -static int cmd_issoundready(int argc, slib_par_t *params, var_t *retval) { +static int cmd_issoundvalid(int argc, slib_par_t *params, var_t *retval) { int result; int sound_id = get_sound_id(argc, params, 0, retval); if (sound_id != -1) { - auto fnResult = IsSoundReady(_soundMap.at(sound_id)); + auto fnResult = IsSoundValid(_soundMap.at(sound_id)); v_setint(retval, fnResult); result = 1; } else { @@ -2316,13 +2349,13 @@ static int cmd_issoundready(int argc, slib_par_t *params, var_t *retval) { } // -// Check if a texture is ready +// Check if a texture is valid (loaded in GPU) // -static int cmd_istextureready(int argc, slib_par_t *params, var_t *retval) { +static int cmd_istexturevalid(int argc, slib_par_t *params, var_t *retval) { int result; int texture_id = get_texture_id(argc, params, 0, retval); if (texture_id != -1) { - auto fnResult = IsTextureReady(_textureMap.at(texture_id)); + auto fnResult = IsTextureValid(_textureMap.at(texture_id)); v_setint(retval, fnResult); result = 1; } else { @@ -2332,13 +2365,13 @@ static int cmd_istextureready(int argc, slib_par_t *params, var_t *retval) { } // -// Checks if wave data is ready +// Checks if wave data is valid (data loaded and parameters) // -static int cmd_iswaveready(int argc, slib_par_t *params, var_t *retval) { +static int cmd_iswavevalid(int argc, slib_par_t *params, var_t *retval) { int result; int wave_id = get_wave_id(argc, params, 0, retval); if (wave_id != -1) { - auto fnResult = IsWaveReady(_waveMap.at(wave_id)); + auto fnResult = IsWaveValid(_waveMap.at(wave_id)); v_setint(retval, fnResult); result = 1; } else { @@ -2348,7 +2381,7 @@ static int cmd_iswaveready(int argc, slib_par_t *params, var_t *retval) { } // -// Check if window is currently focused (only PLATFORM_DESKTOP) +// Check if window is currently focused // static int cmd_iswindowfocused(int argc, slib_par_t *params, var_t *retval) { auto fnResult = IsWindowFocused(); @@ -2366,7 +2399,7 @@ static int cmd_iswindowfullscreen(int argc, slib_par_t *params, var_t *retval) { } // -// Check if window is currently hidden (only PLATFORM_DESKTOP) +// Check if window is currently hidden // static int cmd_iswindowhidden(int argc, slib_par_t *params, var_t *retval) { auto fnResult = IsWindowHidden(); @@ -2375,7 +2408,7 @@ static int cmd_iswindowhidden(int argc, slib_par_t *params, var_t *retval) { } // -// Check if window is currently maximized (only PLATFORM_DESKTOP) +// Check if window is currently maximized // static int cmd_iswindowmaximized(int argc, slib_par_t *params, var_t *retval) { auto fnResult = IsWindowMaximized(); @@ -2384,7 +2417,7 @@ static int cmd_iswindowmaximized(int argc, slib_par_t *params, var_t *retval) { } // -// Check if window is currently minimized (only PLATFORM_DESKTOP) +// Check if window is currently minimized // static int cmd_iswindowminimized(int argc, slib_par_t *params, var_t *retval) { auto fnResult = IsWindowMinimized(); @@ -2682,18 +2715,6 @@ static int cmd_loadimageraw(int argc, slib_par_t *params, var_t *retval) { return 1; } -// -// Load image from SVG file data or string with specified size -// -static int cmd_loadimagesvg(int argc, slib_par_t *params, var_t *retval) { - auto fileNameOrString = get_param_str(argc, params, 0, 0); - auto width = get_param_int(argc, params, 1, 0); - auto height = get_param_int(argc, params, 2, 0); - auto fnResult = LoadImageSvg(fileNameOrString, width, height); - v_setimage(retval, fnResult); - return 1; -} - // // Load model from files (meshes and materials) // diff --git a/raylib/proc-def.h b/raylib/proc-def.h index ce15d14..453dab2 100644 --- a/raylib/proc-def.h +++ b/raylib/proc-def.h @@ -188,7 +188,7 @@ {1, 1, "SETCLIPBOARDTEXT", cmd_setclipboardtext}, {1, 1, "SETCONFIGFLAGS", cmd_setconfigflags}, {1, 1, "SETEXITKEY", cmd_setexitkey}, - {3, 3, "SETGAMEPADVIBRATION", cmd_setgamepadvibration}, + {4, 4, "SETGAMEPADVIBRATION", cmd_setgamepadvibration}, {1, 1, "SETGESTURESENABLED", cmd_setgesturesenabled}, {1, 1, "SETMASTERVOLUME", cmd_setmastervolume}, {3, 3, "SETMODELMESHMATERIAL", cmd_setmodelmeshmaterial}, diff --git a/raylib/proc.h b/raylib/proc.h index 0bf6dea..ab624d6 100644 --- a/raylib/proc.h +++ b/raylib/proc.h @@ -654,7 +654,7 @@ static int cmd_drawmodelwiresex(int argc, slib_par_t *params, var_t *retval) { } // -// Draw a pixel +// Draw a pixel using geometry [Can be slow, use with care] // static int cmd_drawpixel(int argc, slib_par_t *params, var_t *retval) { auto posX = get_param_int(argc, params, 0, 0); @@ -665,7 +665,7 @@ static int cmd_drawpixel(int argc, slib_par_t *params, var_t *retval) { } // -// Draw a pixel (Vector version) +// Draw a pixel using geometry (Vector version) [Can be slow, use with care] // static int cmd_drawpixelv(int argc, slib_par_t *params, var_t *retval) { auto position = get_param_vec2(argc, params, 0); @@ -2286,7 +2286,7 @@ static int cmd_initwindow(int argc, slib_par_t *params, var_t *retval) { } // -// Set window state: maximized, if resizable (only PLATFORM_DESKTOP) +// Set window state: maximized, if resizable // static int cmd_maximizewindow(int argc, slib_par_t *params, var_t *retval) { MaximizeWindow(); @@ -2303,7 +2303,7 @@ static int cmd_memfree(int argc, slib_par_t *params, var_t *retval) { } // -// Set window state: minimized, if resizable (only PLATFORM_DESKTOP) +// Set window state: minimized, if resizable // static int cmd_minimizewindow(int argc, slib_par_t *params, var_t *retval) { MinimizeWindow(); @@ -2427,7 +2427,7 @@ static int cmd_pollinputevents(int argc, slib_par_t *params, var_t *retval) { } // -// Set window state: not minimized/maximized (only PLATFORM_DESKTOP) +// Set window state: not minimized/maximized // static int cmd_restorewindow(int argc, slib_par_t *params, var_t *retval) { RestoreWindow(); @@ -2604,13 +2604,14 @@ static int cmd_setexitkey(int argc, slib_par_t *params, var_t *retval) { } // -// Set gamepad vibration for both motors +// Set gamepad vibration for both motors (duration in seconds) // static int cmd_setgamepadvibration(int argc, slib_par_t *params, var_t *retval) { auto gamepad = get_param_int(argc, params, 0, 0); auto leftMotor = get_param_num(argc, params, 1, 0); auto rightMotor = get_param_num(argc, params, 2, 0); - SetGamepadVibration(gamepad, leftMotor, rightMotor); + auto duration = get_param_num(argc, params, 3, 0); + SetGamepadVibration(gamepad, leftMotor, rightMotor, duration); return 1; } @@ -2921,7 +2922,7 @@ static int cmd_settraceloglevel(int argc, slib_par_t *params, var_t *retval) { } // -// Set window focused (only PLATFORM_DESKTOP) +// Set window focused // static int cmd_setwindowfocused(int argc, slib_par_t *params, var_t *retval) { SetWindowFocused(); @@ -2929,7 +2930,7 @@ static int cmd_setwindowfocused(int argc, slib_par_t *params, var_t *retval) { } // -// Set icon for window (single image, RGBA 32bit, only PLATFORM_DESKTOP) +// Set icon for window (single image, RGBA 32bit) // static int cmd_setwindowicon(int argc, slib_par_t *params, var_t *retval) { int result; @@ -2944,7 +2945,7 @@ static int cmd_setwindowicon(int argc, slib_par_t *params, var_t *retval) { } // -// Set icon for window (multiple images, RGBA 32bit, only PLATFORM_DESKTOP) +// Set icon for window (multiple images, RGBA 32bit) // static int cmd_setwindowicons(int argc, slib_par_t *params, var_t *retval) { int result; @@ -2989,7 +2990,7 @@ static int cmd_setwindowmonitor(int argc, slib_par_t *params, var_t *retval) { } // -// Set window opacity [0.0f..1.0f] (only PLATFORM_DESKTOP) +// Set window opacity [0.0f..1.0f] // static int cmd_setwindowopacity(int argc, slib_par_t *params, var_t *retval) { auto opacity = get_param_num(argc, params, 0, 0); @@ -2998,7 +2999,7 @@ static int cmd_setwindowopacity(int argc, slib_par_t *params, var_t *retval) { } // -// Set window position on screen (only PLATFORM_DESKTOP) +// Set window position on screen // static int cmd_setwindowposition(int argc, slib_par_t *params, var_t *retval) { auto x = get_param_int(argc, params, 0, 0); @@ -3018,7 +3019,7 @@ static int cmd_setwindowsize(int argc, slib_par_t *params, var_t *retval) { } // -// Set window configuration state using flags (only PLATFORM_DESKTOP) +// Set window configuration state using flags // static int cmd_setwindowstate(int argc, slib_par_t *params, var_t *retval) { auto flags = get_param_int(argc, params, 0, 0); @@ -3027,7 +3028,7 @@ static int cmd_setwindowstate(int argc, slib_par_t *params, var_t *retval) { } // -// Set title for window (only PLATFORM_DESKTOP and PLATFORM_WEB) +// Set title for window // static int cmd_setwindowtitle(int argc, slib_par_t *params, var_t *retval) { auto title = get_param_str(argc, params, 0, 0); @@ -3133,7 +3134,7 @@ static int cmd_textappend(int argc, slib_par_t *params, var_t *retval) { } // -// Toggle window state: borderless windowed [resizes window to match monitor resolution] (only PLATFORM_DESKTOP) +// Toggle window state: borderless windowed, resizes window to match monitor resolution // static int cmd_toggleborderlesswindowed(int argc, slib_par_t *params, var_t *retval) { ToggleBorderlessWindowed(); @@ -3141,7 +3142,7 @@ static int cmd_toggleborderlesswindowed(int argc, slib_par_t *params, var_t *ret } // -// Toggle window state: fullscreen/windowed [resizes monitor to match window resolution] (only PLATFORM_DESKTOP) +// Toggle window state: fullscreen/windowed, resizes monitor to match window resolution // static int cmd_togglefullscreen(int argc, slib_par_t *params, var_t *retval) { ToggleFullscreen(); @@ -3493,7 +3494,7 @@ static int cmd_updatemeshbuffer(int argc, slib_par_t *params, var_t *retval) { } // -// Update model animation pose +// Update model animation pose (CPU) // static int cmd_updatemodelanimation(int argc, slib_par_t *params, var_t *retval) { int result; @@ -3510,7 +3511,7 @@ static int cmd_updatemodelanimation(int argc, slib_par_t *params, var_t *retval) } // -// Update model animation mesh bone matrices +// Update model animation mesh bone matrices (GPU skinning) // static int cmd_updatemodelanimationbonematrices(int argc, slib_par_t *params, var_t *retval) { int result; diff --git a/raylib/raygui b/raylib/raygui index 38bc79b..1e03efc 160000 --- a/raylib/raygui +++ b/raylib/raygui @@ -1 +1 @@ -Subproject commit 38bc79b4322cfdbfb3166810f96c6f4f0f2b6cc8 +Subproject commit 1e03efca48c50c5ea4b4a053d5bf04bad58d3e43 diff --git a/raylib/raylib b/raylib/raylib index 13178b9..91a4f04 160000 --- a/raylib/raylib +++ b/raylib/raylib @@ -1 +1 @@ -Subproject commit 13178b9373cebffb97a4ff66b734bacbb9d8758d +Subproject commit 91a4f047942b4eb261c47291496a2cbed03e5c0d diff --git a/websocket/mongoose b/websocket/mongoose index 40ec6b2..d3ffe8c 160000 --- a/websocket/mongoose +++ b/websocket/mongoose @@ -1 +1 @@ -Subproject commit 40ec6b263ddaf217104edfde220b8ae3cade039d +Subproject commit d3ffe8c6470faa238e2a466b0aa9e064657c5a5a From fc4e1873d027f0b26b59c5f26051c819eac47319 Mon Sep 17 00:00:00 2001 From: chrisws Date: Sat, 1 Mar 2025 19:42:59 +1030 Subject: [PATCH 058/131] Update dependencies --- gtk-server/uthash | 2 +- nuklear/Nuklear | 2 +- raylib/README.md | 18 ++++++++++-------- raylib/func-def.h | 2 ++ raylib/func.h | 23 +++++++++++++++++++++-- raylib/proc-def.h | 2 +- raylib/proc.h | 6 +++--- raylib/raygui | 2 +- raylib/raylib | 2 +- websocket/mongoose | 2 +- 10 files changed, 42 insertions(+), 19 deletions(-) diff --git a/gtk-server/uthash b/gtk-server/uthash index f69112c..41c357f 160000 --- a/gtk-server/uthash +++ b/gtk-server/uthash @@ -1 +1 @@ -Subproject commit f69112c04f1b6e059b8071cb391a1fcc83791a00 +Subproject commit 41c357fd74ade4f4b4822c4407d2f51c4558e18d diff --git a/nuklear/Nuklear b/nuklear/Nuklear index 6566d90..c98aa92 160000 --- a/nuklear/Nuklear +++ b/nuklear/Nuklear @@ -1 +1 @@ -Subproject commit 6566d9075d5fed48af014c93f87c4aed8c4bd21c +Subproject commit c98aa9247bb2354a2afc126f59d5fc6a45fe3c73 diff --git a/raylib/README.md b/raylib/README.md index bb15020..b0f9723 100644 --- a/raylib/README.md +++ b/raylib/README.md @@ -1,10 +1,10 @@ -*Raylib* _MAJOR 5 _MINOR 5 _PATCH 0 5.5 +*Raylib* _MAJOR 5 _MINOR 6 _PATCH 0 5.6-dev ======= raylib is a simple and easy-to-use library to enjoy videogames programming. https://www.raylib.com/ -Implemented APIs (631) +Implemented APIs (633) ---------------- | Name | Description | @@ -200,6 +200,7 @@ Implemented APIs (631) | func GetCameraMatrix(camera) | Get camera transform matrix (view matrix) | | func GetCameraMatrix2D(camera) | Get camera 2d transform matrix | | func GetCharPressed() | Get char pressed (unicode), call it multiple times for chars queued, returns 0 when the queue is empty | +| func GetClipboardImage() | Get clipboard image content | | func GetClipboardText() | Get clipboard text content | | func GetCodepoint(text, codepointSize) | Get next codepoint in a UTF-8 encoded string, 0x3f('?') is returned on failure | | func GetCodepointCount(text) | Get total number of codepoints in a UTF-8 encoded string | @@ -231,6 +232,7 @@ Implemented APIs (631) | func GetGlyphIndex(font, codepoint) | Get glyph index position in font for a codepoint (unicode character), fallback to '?' if not found | | func GetImageAlphaBorder(image, threshold) | Get image alpha border rectangle | | func GetImageColor(image, x, y) | Get image pixel color at (x, y) position | +| func GetKeyName(key) | Get name of a QWERTY key on the current keyboard layout (eg returns string 'q' for KEY_A on an AZERTY keyboard) | | func GetKeyPressed() | Get key pressed (keycode), call it multiple times for keys queued, returns 0 when the queue is empty | | func GetMasterVolume() | Get master volume (listener) | | func GetModelBoundingBox(model) | Compute model bounding box limits (considers all meshes) | @@ -550,7 +552,7 @@ Implemented APIs (631) | sub SetRandomSeed(seed) | Set the seed for the random number generator | | sub SetShaderValue(shader, locIndex, value, uniformType) | Set shader uniform value | | sub SetShaderValueMatrix(shader, locIndex, mat) | Set shader uniform value (matrix 4x4) | -| sub SetShaderValueTexture(shader, locIndex, texture) | Set shader uniform value for texture (sampler2d) | +| sub SetShaderValueTexture(shader, locIndex, texture) | Set shader uniform value and bind the texture (sampler2d) | | sub SetShaderValueV(shader, locIndex, value, uniformType, count) | Set shader uniform value vector | | sub SetShapesTexture(texture, source) | Set texture and rectangle to be used on shapes drawing | | sub SetSoundPan(sound, pan) | Set pan for a sound (0.5 is center) | @@ -590,8 +592,8 @@ Implemented APIs (631) | func TextReplace(text, replace, by) | Replace text string (WARNING: memory must be freed!) | | func TextSubtext(text, position, length) | Get a piece of a text string | | func TextToCamel(text) | Get Camel case notation version of provided string | -| func TextToFloat(text) | Get float value from text (negative values not supported) | -| func TextToInteger(text) | Get integer value from text (negative values not supported) | +| func TextToFloat(text) | Get float value from text | +| func TextToInteger(text) | Get integer value from text | | func TextToLower(text) | Get lower case version of provided string | | func TextToPascal(text) | Get Pascal case notation version of provided string | | func TextToSnake(text) | Get Snake case notation version of provided string | @@ -627,7 +629,7 @@ Implemented APIs (631) | sub UpdateCamera(camera, mode) | Update camera position for selected mode | | sub UpdateMeshBuffer(mesh, index, data, dataSize, offset) | Update mesh vertex data in GPU for a specific buffer index | | sub UpdateModelAnimation(model, anim, frame) | Update model animation pose (CPU) | -| sub UpdateModelAnimationBoneMatrices(model, anim, frame) | Update model animation mesh bone matrices (GPU skinning) | +| sub UpdateModelAnimationBones(model, anim, frame) | Update model animation mesh bone matrices (GPU skinning) | | sub UpdateMusicStream(music) | Updates buffers for music streaming | | func updatePhysics() | n/a | | sub UpdateSound(sound, data, sampleCount) | Update sound buffer with new data | @@ -646,8 +648,8 @@ Unimplemented APIs | Name | Description | |---------|---------------| -| AttachAudioMixedProcessor | Attach audio stream processor to the entire audio pipeline, receives the samples as 'float' | -| AttachAudioStreamProcessor | Attach audio stream processor to stream, receives the samples as 'float' | +| AttachAudioMixedProcessor | Attach audio stream processor to the entire audio pipeline, receives frames x 2 samples as 'float' (stereo) | +| AttachAudioStreamProcessor | Attach audio stream processor to stream, receives frames x 2 samples as 'float' (stereo) | | BeginVrStereoMode | Begin stereo rendering (requires VR simulator) | | DetachAudioMixedProcessor | Detach audio stream processor from the entire audio pipeline | | DetachAudioStreamProcessor | Detach audio stream processor from stream | diff --git a/raylib/func-def.h b/raylib/func-def.h index 9377efb..831f691 100644 --- a/raylib/func-def.h +++ b/raylib/func-def.h @@ -69,6 +69,7 @@ {1, 1, "GETCAMERAMATRIX", cmd_getcameramatrix}, {1, 1, "GETCAMERAMATRIX2D", cmd_getcameramatrix2d}, {0, 0, "GETCHARPRESSED", cmd_getcharpressed}, + {0, 0, "GETCLIPBOARDIMAGE", cmd_getclipboardimage}, {0, 0, "GETCLIPBOARDTEXT", cmd_getclipboardtext}, {1, 1, "GETCODEPOINT", cmd_getcodepoint}, {1, 1, "GETCODEPOINTCOUNT", cmd_getcodepointcount}, @@ -100,6 +101,7 @@ {2, 2, "GETGLYPHINDEX", cmd_getglyphindex}, {2, 2, "GETIMAGEALPHABORDER", cmd_getimagealphaborder}, {3, 3, "GETIMAGECOLOR", cmd_getimagecolor}, + {1, 1, "GETKEYNAME", cmd_getkeyname}, {0, 0, "GETKEYPRESSED", cmd_getkeypressed}, {0, 0, "GETMASTERVOLUME", cmd_getmastervolume}, {1, 1, "GETMODELBOUNDINGBOX", cmd_getmodelboundingbox}, diff --git a/raylib/func.h b/raylib/func.h index 51912f8..cc97d4a 100644 --- a/raylib/func.h +++ b/raylib/func.h @@ -894,6 +894,15 @@ static int cmd_getcharpressed(int argc, slib_par_t *params, var_t *retval) { return 1; } +// +// Get clipboard image content +// +static int cmd_getclipboardimage(int argc, slib_par_t *params, var_t *retval) { + auto fnResult = GetClipboardImage(); + v_setimage(retval, fnResult); + return 1; +} + // // Get clipboard text content // @@ -1226,6 +1235,16 @@ static int cmd_getimagecolor(int argc, slib_par_t *params, var_t *retval) { return result; } +// +// Get name of a QWERTY key on the current keyboard layout (eg returns string 'q' for KEY_A on an AZERTY keyboard) +// +static int cmd_getkeyname(int argc, slib_par_t *params, var_t *retval) { + auto key = get_param_int(argc, params, 0, 0); + auto fnResult = (const char *)GetKeyName(key); + v_setstr(retval, fnResult); + return 1; +} + // // Get key pressed (keycode), call it multiple times for keys queued, returns 0 when the queue is empty // @@ -3104,7 +3123,7 @@ static int cmd_texttocamel(int argc, slib_par_t *params, var_t *retval) { } // -// Get float value from text (negative values not supported) +// Get float value from text // static int cmd_texttofloat(int argc, slib_par_t *params, var_t *retval) { auto text = get_param_str(argc, params, 0, 0); @@ -3114,7 +3133,7 @@ static int cmd_texttofloat(int argc, slib_par_t *params, var_t *retval) { } // -// Get integer value from text (negative values not supported) +// Get integer value from text // static int cmd_texttointeger(int argc, slib_par_t *params, var_t *retval) { auto text = get_param_str(argc, params, 0, 0); diff --git a/raylib/proc-def.h b/raylib/proc-def.h index 453dab2..f509533 100644 --- a/raylib/proc-def.h +++ b/raylib/proc-def.h @@ -262,7 +262,7 @@ {3, 3, "UPDATEAUDIOSTREAM", cmd_updateaudiostream}, {5, 5, "UPDATEMESHBUFFER", cmd_updatemeshbuffer}, {3, 3, "UPDATEMODELANIMATION", cmd_updatemodelanimation}, - {3, 3, "UPDATEMODELANIMATIONBONEMATRICES", cmd_updatemodelanimationbonematrices}, + {3, 3, "UPDATEMODELANIMATIONBONES", cmd_updatemodelanimationbones}, {1, 1, "UPDATEMUSICSTREAM", cmd_updatemusicstream}, {3, 3, "UPDATESOUND", cmd_updatesound}, {3, 3, "UPDATETEXTUREREC", cmd_updatetexturerec}, diff --git a/raylib/proc.h b/raylib/proc.h index ab624d6..ce6790c 100644 --- a/raylib/proc.h +++ b/raylib/proc.h @@ -2769,7 +2769,7 @@ static int cmd_setshadervaluematrix(int argc, slib_par_t *params, var_t *retval) } // -// Set shader uniform value for texture (sampler2d) +// Set shader uniform value and bind the texture (sampler2d) // static int cmd_setshadervaluetexture(int argc, slib_par_t *params, var_t *retval) { int result; @@ -3513,13 +3513,13 @@ static int cmd_updatemodelanimation(int argc, slib_par_t *params, var_t *retval) // // Update model animation mesh bone matrices (GPU skinning) // -static int cmd_updatemodelanimationbonematrices(int argc, slib_par_t *params, var_t *retval) { +static int cmd_updatemodelanimationbones(int argc, slib_par_t *params, var_t *retval) { int result; int model_id = get_model_id(argc, params, 0, retval); int anim_id = get_model_animation_id(argc, params, 1, retval); if (model_id != -1 && anim_id != -1) { auto frame = get_param_int(argc, params, 2, 0); - UpdateModelAnimationBoneMatrices(_modelMap.at(model_id), _modelAnimationMap.at(anim_id), frame); + UpdateModelAnimationBones(_modelMap.at(model_id), _modelAnimationMap.at(anim_id), frame); result = 1; } else { result = 0; diff --git a/raylib/raygui b/raylib/raygui index 1e03efc..9a95871 160000 --- a/raylib/raygui +++ b/raylib/raygui @@ -1 +1 @@ -Subproject commit 1e03efca48c50c5ea4b4a053d5bf04bad58d3e43 +Subproject commit 9a95871701a5fc63bea35eab73fef6414e048b73 diff --git a/raylib/raylib b/raylib/raylib index 91a4f04..eb8a343 160000 --- a/raylib/raylib +++ b/raylib/raylib @@ -1 +1 @@ -Subproject commit 91a4f047942b4eb261c47291496a2cbed03e5c0d +Subproject commit eb8a343e313967a51cb302ac9bb1206a05727d13 diff --git a/websocket/mongoose b/websocket/mongoose index d3ffe8c..55bc610 160000 --- a/websocket/mongoose +++ b/websocket/mongoose @@ -1 +1 @@ -Subproject commit d3ffe8c6470faa238e2a466b0aa9e064657c5a5a +Subproject commit 55bc6105e148633ddc65bddbdb307f1477c0fc01 From f3b6626f72f6756e1582fb362fef240ae6eebf85 Mon Sep 17 00:00:00 2001 From: chrisws Date: Sat, 1 Mar 2025 19:44:03 +1030 Subject: [PATCH 059/131] RAYLIB: space shooter game created by ChatGPT and updated by Claude --- raylib/samples/space-fighter-3d.bas | 781 ++++++++++++++++++++++++++++ 1 file changed, 781 insertions(+) create mode 100644 raylib/samples/space-fighter-3d.bas diff --git a/raylib/samples/space-fighter-3d.bas b/raylib/samples/space-fighter-3d.bas new file mode 100644 index 0000000..8dcd895 --- /dev/null +++ b/raylib/samples/space-fighter-3d.bas @@ -0,0 +1,781 @@ +' +' Created by ChatGPT, updated by Claude +' + +' Load raylib plugin +IMPORT raylib as rl +IMPORT raylibc as c +' Screen size +CONST SCREEN_WIDTH = 800 +CONST SCREEN_HEIGHT = 600 +' Initialize the game window +rl.initWindow(SCREEN_WIDTH, SCREEN_HEIGHT, "3D Space Fighter") +rl.setTargetFPS(60) +' Camera setup (First-person) +DIM camera +camera.position = [0, 2, 10] +camera.target = [0, 2, 0] +camera.up = [0, 1, 0] +camera.fovy = 75 +camera.projection = c.CAMERA_PERSPECTIVE +' Player properties +LET playerX = 0 +LET playerY = 0 +LET playerZ = -5 +CONST BASE_PLAYER_SPEED = 0.08 +LET playerSpeed = BASE_PLAYER_SPEED +LET playerRotation = 0 ' Player rotation for banking effect +CONST playerSize = 1.0 +LET playerHealth = 100 +LET playerShieldActive = FALSE +LET playerShieldTimer = 0 +LET playerShieldCooldown = 0 +CONST SHIELD_DURATION = 180 ' 3 seconds at 60 FPS +CONST SHIELD_COOLDOWN = 300 ' 5 seconds at 60 FPS +' Powerup properties +CONST MAX_POWERUPS = 3 +DIM powerupX[MAX_POWERUPS] +DIM powerupY[MAX_POWERUPS] +DIM powerupZ[MAX_POWERUPS] +DIM powerupType[MAX_POWERUPS] ' 1=Speed, 2=Shield, 3=Weapon Upgrade +DIM powerupActive[MAX_POWERUPS] +' Enemy properties +CONST MAX_ENEMIES = 8 +DIM enemyX[MAX_ENEMIES] +DIM enemyY[MAX_ENEMIES] +DIM enemyZ[MAX_ENEMIES] +DIM enemySize[MAX_ENEMIES] +DIM enemyType[MAX_ENEMIES] ' 0=Regular, 1=Fast, 2=Boss +DIM enemyHealth[MAX_ENEMIES] +DIM enemySpeed[MAX_ENEMIES] +' Bullet properties +CONST MAX_BULLETS = 20 +DIM bulletX[MAX_BULLETS] +DIM bulletY[MAX_BULLETS] +DIM bulletZ[MAX_BULLETS] +DIM bulletActive[MAX_BULLETS] +DIM bulletType[MAX_BULLETS] ' 0=Regular, 1=Upgraded +LET bulletSpeed = 0.3 +LET bulletIndex = 0 +LET bulletCooldown = 0 +LET weaponUpgraded = FALSE +LET weaponUpgradeTimer = 0 +CONST WEAPON_UPGRADE_DURATION = 600 ' 10 seconds at 60 FPS +' Particle effects +CONST MAX_PARTICLES = 100 +DIM particleX[MAX_PARTICLES] +DIM particleY[MAX_PARTICLES] +DIM particleZ[MAX_PARTICLES] +DIM particleSpeedX[MAX_PARTICLES] +DIM particleSpeedY[MAX_PARTICLES] +DIM particleSpeedZ[MAX_PARTICLES] +DIM particleLife[MAX_PARTICLES] +DIM particleColor[MAX_PARTICLES] +DIM particleSize[MAX_PARTICLES] +DIM particleActive[MAX_PARTICLES] +LET nextParticle = 0 +' Background stars +CONST MAX_STARS = 200 +DIM starX[MAX_STARS] +DIM starY[MAX_STARS] +DIM starZ[MAX_STARS] +DIM starSize[MAX_STARS] +' Score and game state +LET score = 0 +LET highScore = 0 +LET gameOver = FALSE +LET level = 1 +LET enemiesDefeated = 0 +LET levelEnemyCount = 10 ' Enemies to defeat before level increases +LET bossFight = FALSE +LET bossIndex = -1 +LET gameStarted = FALSE +LET gamePaused = FALSE +' Sound (placeholders - would need actual sound implementation) +LET soundEnabled = TRUE +' Initialize stars +FOR i = 0 TO MAX_STARS - 1 + starX[i] = (RND() - 0.5) * 100 + starY[i] = (RND() - 0.5) * 100 + starZ[i] = -50 - RND() * 150 + starSize[i] = 0.1 + RND() * 0.3 +NEXT +' Initialize enemies +SUB InitializeEnemies() + FOR i = 0 TO MAX_ENEMIES - 1 + ' Determine enemy type (chance for special types increases with level) + LET typeRoll = RND() + IF bossFight AND i = 0 THEN + enemyType[i] = 2 ' Boss + enemySize[i] = 4 + (level * 0.5) + enemyHealth[i] = 10 + (level * 5) + enemySpeed[i] = 0.05 + bossIndex = i + ELSEIF typeRoll > 0.8 - (level * 0.05) THEN + enemyType[i] = 1 ' Fast enemy + enemySize[i] = 1 + RND() + enemyHealth[i] = 1 + enemySpeed[i] = 0.15 + (level * 0.01) + ELSE + enemyType[i] = 0 ' Regular enemy + enemySize[i] = 2 + RND() + enemyHealth[i] = 2 + enemySpeed[i] = 0.08 + (level * 0.005) + ENDIF + + enemyX[i] = (RND() - 0.5) * 20 + enemyY[i] = (RND() - 0.5) * 10 + enemyZ[i] = -30 - RND() * 50 + NEXT +END SUB +' Initialize powerups +SUB InitializePowerups() + FOR i = 0 TO MAX_POWERUPS - 1 + powerupActive[i] = FALSE + NEXT +END SUB +' Create particle effect +SUB CreateExplosion(x, y, z, size, _color) + local i + FOR i = 0 TO 15 + LET idx = nextParticle + particleX[idx] = x + particleY[idx] = y + particleZ[idx] = z + particleSpeedX[idx] = (RND() - 0.5) * 0.3 + particleSpeedY[idx] = (RND() - 0.5) * 0.3 + particleSpeedZ[idx] = (RND() - 0.5) * 0.3 + particleLife[idx] = 30 + RND() * 30 + particleColor[idx] = _color + particleSize[idx] = size * (0.3 + RND() * 0.3) + particleActive[idx] = TRUE + nextParticle = (nextParticle + 1) MOD MAX_PARTICLES + NEXT +END SUB +' Spawn powerup with chance +SUB TrySpawnPowerup(x, y, z) + local i + IF RND() > 0.7 THEN ' 30% chance to spawn a powerup + FOR i = 0 TO MAX_POWERUPS - 1 + IF NOT powerupActive[i] THEN + powerupX[i] = x + powerupY[i] = y + powerupZ[i] = z + powerupType[i] = INT(RND() * 3) + 1 ' 1=Speed, 2=Shield, 3=Weapon + powerupActive[i] = TRUE + EXIT FOR + ENDIF + NEXT + ENDIF +END SUB + +' Get color for enemy type +FUNC GetEnemyColor(typex, health, maxHealth) + SELECT CASE typex + CASE 0: ' Regular + RETURN rl.colorToInt([255, 0, 0, 255]) + CASE 1: ' Fast + RETURN rl.colorToInt([255, 165, 0, 255]) + CASE 2: ' Boss + ' Boss changes color based on health + LET healthPercent = health / maxHealth + IF healthPercent > 0.7 THEN + RETURN rl.colorToInt([128, 0, 128, 255]) ' Purple + ELSEIF healthPercent > 0.3 THEN + RETURN rl.colorToInt([255, 0, 128, 255]) ' Pink + ELSE + RETURN rl.colorToInt([255, 0, 0, 255]) ' Red + ENDIF + END SELECT +END + +' Draw health bar above enemy +SUB DrawHealthBar(x, y, z, health, maxHealth, size) + LET barWidth = size * 2 + LET healthPercent = health / maxHealth + LET healthBarWidth = barWidth * healthPercent + + ' Background of health bar + rl.drawCube([x, y + size + 0.2, z], barWidth, 0.1, 0.1, c.GRAY) + + ' Health portion + IF healthPercent > 0 THEN + LET healthColor = c.GREEN + IF healthPercent < 0.3 THEN + healthColor = c.RED + ELSEIF healthPercent < 0.6 THEN + healthColor = c.YELLOW + ENDIF + + rl.drawCube([x - (barWidth - healthBarWidth)/2, y + size + 0.2, z], healthBarWidth, 0.1, 0.1, healthColor) + ENDIF +END SUB +' Start new game +SUB StartNewGame() + + score = 0 + gameOver = FALSE + playerX = 0 + playerY = 0 + playerZ = -5 + playerHealth = 100 + playerSpeed = BASE_PLAYER_SPEED + playerShieldActive = FALSE + playerShieldTimer = 0 + playerShieldCooldown = 0 + weaponUpgraded = FALSE + weaponUpgradeTimer = 0 + level = 1 + enemiesDefeated = 0 + levelEnemyCount = 10 + bossFight = FALSE + bossIndex = -1 + + ' Reset enemies and powerups + InitializeEnemies() + InitializePowerups() + + ' Clear bullets and particles + FOR i = 0 TO MAX_BULLETS - 1 + bulletActive[i] = FALSE + NEXT + + FOR i = 0 TO MAX_PARTICLES - 1 + particleActive[i] = FALSE + NEXT + + gameStarted = TRUE + gamePaused = FALSE +END SUB + +' Main game loop +WHILE NOT rl.windowShouldClose() + ' Handle game state + IF NOT gameStarted THEN + ' Title screen + rl.beginDrawing() + rl.clearBackground(c.BLACK) + + ' Draw stars in the background + FOR i = 0 TO MAX_STARS - 1 + LET starDepth = 1.0 - (starZ[i] / -200) + LET brightness = 100 + starDepth * 155 + rl.drawCircle(SCREEN_WIDTH/2 + starX[i], SCREEN_HEIGHT/2 + starY[i], starSize[i] * starDepth, rl.colorToInt([brightness, brightness, brightness, 255])) + starZ[i] += 0.2 + IF starZ[i] > 10 THEN + starZ[i] = -200 + starX[i] = (RND() - 0.5) * 100 + starY[i] = (RND() - 0.5) * 100 + ENDIF + NEXT + + rl.drawText("3D SPACE FIGHTER", SCREEN_WIDTH/2 - 180, SCREEN_HEIGHT/3, 40, c.YELLOW) + rl.drawText("Press ENTER to start", SCREEN_WIDTH/2 - 120, SCREEN_HEIGHT/2, 20, c.WHITE) + rl.drawText("Arrow Keys: Move SPACE: Shoot", SCREEN_WIDTH/2 - 170, SCREEN_HEIGHT/2 + 60, 20, c.LIGHTGRAY) + rl.drawText("S: Shield P: Pause", SCREEN_WIDTH/2 - 120, SCREEN_HEIGHT/2 + 90, 20, c.LIGHTGRAY) + + IF highScore > 0 THEN + rl.drawText("High Score: " + highScore, SCREEN_WIDTH/2 - 80, SCREEN_HEIGHT/2 + 140, 20, c.GREEN) + ENDIF + + rl.endDrawing() + + IF rl.isKeyPressed(c.KEY_ENTER) THEN + StartNewGame() + ENDIF + ELSEIF gamePaused THEN + ' Pause menu + rl.beginDrawing() + rl.clearBackground(c.BLACK) + rl.drawText("PAUSED", SCREEN_WIDTH/2 - 60, SCREEN_HEIGHT/2 - 30, 40, c.YELLOW) + rl.drawText("Press P to resume", SCREEN_WIDTH/2 - 100, SCREEN_HEIGHT/2 + 30, 20, c.WHITE) + rl.endDrawing() + + IF rl.isKeyPressed(c.KEY_P) THEN + gamePaused = FALSE + ENDIF + ELSEIF NOT gameOver THEN + ' Toggle pause + IF rl.isKeyPressed(c.KEY_P) THEN + gamePaused = NOT gamePaused + 'CONTINUE + ENDIF + + ' Player movement + LET oldPlayerX = playerX + IF rl.isKeyDown(c.KEY_LEFT) THEN + playerX -= playerSpeed + playerRotation = -0.3 ' Bank left + ELSEIF rl.isKeyDown(c.KEY_RIGHT) THEN + playerX += playerSpeed + playerRotation = 0.3 ' Bank right + ELSE + playerRotation = playerRotation * 0.9 ' Return to level + ENDIF + + IF rl.isKeyDown(c.KEY_UP) THEN playerY += playerSpeed + IF rl.isKeyDown(c.KEY_DOWN) THEN playerY -= playerSpeed + + ' Keep player within bounds + IF playerX < -10 THEN playerX = -10 + IF playerX > 10 THEN playerX = 10 + IF playerY < -8 THEN playerY = -8 + IF playerY > 8 THEN playerY = 8 + + ' Shield activation + IF rl.isKeyPressed(c.KEY_S) AND playerShieldCooldown <= 0 THEN + playerShieldActive = TRUE + playerShieldTimer = SHIELD_DURATION + playerShieldCooldown = SHIELD_DURATION + SHIELD_COOLDOWN + ENDIF + + ' Update shield timer + IF playerShieldActive THEN + playerShieldTimer -= 1 + IF playerShieldTimer <= 0 THEN + playerShieldActive = FALSE + ENDIF + ENDIF + + ' Update shield cooldown + IF playerShieldCooldown > 0 THEN + playerShieldCooldown -= 1 + ENDIF + + ' Update weapon upgrade timer + IF weaponUpgraded THEN + weaponUpgradeTimer -= 1 + IF weaponUpgradeTimer <= 0 THEN + weaponUpgraded = FALSE + ENDIF + ENDIF + + ' Bullet firing + IF bulletCooldown > 0 THEN + bulletCooldown -= 1 + ENDIF + + IF rl.isKeyDown(c.KEY_SPACE) AND bulletCooldown <= 0 THEN + ' Regular fire + bulletX[bulletIndex] = playerX + bulletY[bulletIndex] = playerY + bulletZ[bulletIndex] = playerZ + bulletActive[bulletIndex] = TRUE + bulletType[bulletIndex] = IFF(weaponUpgraded, 1, 0) + bulletIndex = (bulletIndex + 1) MOD MAX_BULLETS + + ' If weapon upgraded, fire additional bullets + IF weaponUpgraded THEN + ' Left bullet + bulletX[bulletIndex] = playerX - 0.5 + bulletY[bulletIndex] = playerY + bulletZ[bulletIndex] = playerZ + bulletActive[bulletIndex] = TRUE + bulletType[bulletIndex] = 1 + bulletIndex = (bulletIndex + 1) MOD MAX_BULLETS + + ' Right bullet + bulletX[bulletIndex] = playerX + 0.5 + bulletY[bulletIndex] = playerY + bulletZ[bulletIndex] = playerZ + bulletActive[bulletIndex] = TRUE + bulletType[bulletIndex] = 1 + bulletIndex = (bulletIndex + 1) MOD MAX_BULLETS + + bulletCooldown = 8 ' Shorter cooldown for upgraded weapon + ELSE + bulletCooldown = 15 + ENDIF + ENDIF + + ' Move bullets + FOR i = 0 TO MAX_BULLETS - 1 + IF bulletActive[i] THEN + bulletZ[i] -= bulletSpeed + IF bulletZ[i] < -100 THEN bulletActive[i] = FALSE + ENDIF + NEXT + + ' Move powerups + FOR i = 0 TO MAX_POWERUPS - 1 + IF powerupActive[i] THEN + powerupZ[i] += 0.05 + + ' Check if player collected powerup + IF ABS(powerupX[i] - playerX) < 1.5 AND ABS(powerupY[i] - playerY) < 1.5 AND ABS(powerupZ[i] - playerZ) < 1.5 THEN + SELECT CASE powerupType[i] + CASE 1: ' Speed boost + playerSpeed = BASE_PLAYER_SPEED * 1.5 + ' Create speed boost timer (reset after 10 seconds) + DELAY 600 + playerSpeed = BASE_PLAYER_SPEED + + CASE 2: ' Shield + playerShieldActive = TRUE + playerShieldTimer = SHIELD_DURATION + playerShieldCooldown = SHIELD_DURATION + SHIELD_COOLDOWN + + CASE 3: ' Weapon upgrade + weaponUpgraded = TRUE + weaponUpgradeTimer = WEAPON_UPGRADE_DURATION + END SELECT + + powerupActive[i] = FALSE + score += 5 ' Bonus for collecting powerup + ENDIF + + ' Remove powerup if it goes past the player + IF powerupZ[i] > 10 THEN + powerupActive[i] = FALSE + ENDIF + ENDIF + NEXT + + ' Move and update enemies + FOR i = 0 TO MAX_ENEMIES - 1 + ' Boss movement is different - more complex pattern + IF enemyType[i] = 2 THEN ' Boss + enemyZ[i] += enemySpeed[i] * 0.5 + + ' Boss moves in a figure-8 pattern + LET timex = rl.getTime() * 0.5 + enemyX[i] = SIN(timex) * 5 + enemyY[i] = SIN(timex * 2) * 3 + + ' Boss occasionally fires at player + IF RND() < 0.02 THEN ' 2% chance per frame + FOR j = 0 TO 2 ' Fire 3 bullets + TrySpawnPowerup(enemyX[i], enemyY[i], enemyZ[i]) + NEXT + ENDIF + ELSE + ' Regular enemies move toward the player with some randomness + enemyZ[i] += enemySpeed[i] + + ' Add some side-to-side movement + IF enemyType[i] = 1 THEN ' Fast enemies are more erratic + enemyX[i] += SIN(rl.getTime() * 2 + i) * 0.05 + enemyY[i] += COS(rl.getTime() * 3 + i) * 0.05 + ELSE + enemyX[i] += SIN(rl.getTime() + i) * 0.02 + enemyY[i] += COS(rl.getTime() * 1.5 + i) * 0.02 + ENDIF + ENDIF + + ' Reset enemy when it reaches past the player + IF enemyZ[i] > 15 THEN + enemyX[i] = (RND() - 0.5) * 20 + enemyY[i] = (RND() - 0.5) * 10 + enemyZ[i] = -30 - RND() * 20 + + ' Penalize score for missed enemies + IF NOT bossFight OR i != bossIndex THEN + score = MAX(0, score - 5) + ENDIF + ENDIF + + ' Check collision with bullets + FOR j = 0 TO MAX_BULLETS - 1 + IF bulletActive[j] AND ABS(enemyX[i] - bulletX[j]) < enemySize[i] * 0.8 AND ABS(enemyY[i] - bulletY[j]) < enemySize[i] * 0.8 AND ABS(enemyZ[i] - bulletZ[j]) < enemySize[i] THEN + ' Enemy hit by bullet + bulletActive[j] = FALSE + + ' Reduce enemy health + enemyHealth[i] -= IFF(bulletType[j] = 1, 2, 1) ' Upgraded bullets do double damage + + ' Create hit particle effect + CreateExplosion(bulletX[j], bulletY[j], bulletZ[j], 0.5, rl.colorToInt([255, 255, 0, 255])) + + ' Check if enemy is destroyed + IF enemyHealth[i] <= 0 THEN + ' Create explosion effect + CreateExplosion(enemyX[i], enemyY[i], enemyZ[i], enemySize[i], GetEnemyColor(enemyType[i], 1, 1)) + + ' Chance to spawn powerup + TrySpawnPowerup(enemyX[i], enemyY[i], enemyZ[i]) + + ' Reset enemy + IF enemyType[i] = 2 THEN ' Boss defeated + bossFight = FALSE + bossIndex = -1 + score += 50 ' Big bonus for boss + level += 1 + + ' Reset all enemies with higher difficulty + InitializeEnemies() + ELSE + enemyX[i] = (RND() - 0.5) * 20 + enemyY[i] = (RND() - 0.5) * 10 + enemyZ[i] = -30 - RND() * 20 + + ' Determine enemy type (chance for special types increases with level) + LET typeRoll = RND() + IF typeRoll > 0.8 - (level * 0.05) THEN + enemyType[i] = 1 ' Fast enemy + enemySize[i] = 1 + RND() + enemyHealth[i] = 1 + enemySpeed[i] = 0.15 + (level * 0.01) + ELSE + enemyType[i] = 0 ' Regular enemy + enemySize[i] = 2 + RND() + enemyHealth[i] = 2 + enemySpeed[i] = 0.08 + (level * 0.005) + ENDIF + + ' Update counters + enemiesDefeated += 1 + + ' Score based on enemy type + IF enemyType[i] = 1 THEN ' Fast enemy worth more + score += 15 + ELSE + score += 10 + ENDIF + ENDIF + ENDIF + ENDIF + NEXT + + ' Check collision with player + IF NOT playerShieldActive AND ABS(enemyX[i] - playerX) < (enemySize[i] + playerSize) * 0.7 AND ABS(enemyY[i] - playerY) < (enemySize[i] + playerSize) * 0.7 AND ABS(enemyZ[i] - playerZ) < (enemySize[i] + playerSize) * 0.7 THEN + ' Player hit by enemy + playerHealth -= IFF(enemyType[i] = 2, 25, 10) ' Boss does more damage + + ' Create explosion effect + CreateExplosion(playerX, playerY, playerZ, 1.0, c.RED) + + ' Reset enemy position + enemyX[i] = (RND() - 0.5) * 20 + enemyY[i] = (RND() - 0.5) * 10 + enemyZ[i] = -30 - RND() * 20 + + ' Check if player is destroyed + IF playerHealth <= 0 THEN + gameOver = TRUE + IF score > highScore THEN + highScore = score + ENDIF + ENDIF + ENDIF + NEXT + + ' Check for level up + IF NOT bossFight AND enemiesDefeated >= levelEnemyCount THEN + ' Start boss fight + bossFight = TRUE + InitializeEnemies() ' This will set up the boss + enemiesDefeated = 0 + levelEnemyCount = 10 + (level * 5) ' More enemies needed for next level + ENDIF + + ' Update particles + FOR i = 0 TO MAX_PARTICLES - 1 + IF particleActive[i] THEN + particleX[i] += particleSpeedX[i] + particleY[i] += particleSpeedY[i] + particleZ[i] += particleSpeedZ[i] + particleLife[i] -= 1 + + IF particleLife[i] <= 0 THEN + particleActive[i] = FALSE + ENDIF + ENDIF + NEXT + + ' Update stars (moving background) + FOR i = 0 TO MAX_STARS - 1 + starZ[i] += 0.2 + IF starZ[i] > 10 THEN + starZ[i] = -150 + starX[i] = (RND() - 0.5) * 100 + starY[i] = (RND() - 0.5) * 100 + ENDIF + NEXT + + ' Drawing + rl.beginDrawing() + rl.clearBackground(c.BLACK) + + ' Draw stars in background + FOR i = 0 TO MAX_STARS - 1 + LET distFactor = 1.0 - (starZ[i] / -150) + LET starScreenX = SCREEN_WIDTH/2 + (starX[i] - camera.position[0]) * distFactor * 5 + LET starScreenY = SCREEN_HEIGHT/2 + (starY[i] - camera.position[1]) * distFactor * 5 + + IF starScreenX > 0 AND starScreenX < SCREEN_WIDTH AND starScreenY > 0 AND starScreenY < SCREEN_HEIGHT THEN + LET brightness = 100 + distFactor * 155 + rl.drawCircle(starScreenX, starScreenY, starSize[i] * distFactor, rl.colorToInt([brightness, brightness, brightness, 255])) + ENDIF + NEXT + + rl.beginMode3D(camera) + + ' Draw particles + FOR i = 0 TO MAX_PARTICLES - 1 + IF particleActive[i] THEN + rl.drawSphere([particleX[i], particleY[i], particleZ[i]], particleSize[i], particleColor[i]) + ENDIF + NEXT + + ' Draw powerups + FOR i = 0 TO MAX_POWERUPS - 1 + IF powerupActive[i] THEN + SELECT CASE powerupType[i] + CASE 1: ' Speed - blue + rl.drawCube([powerupX[i], powerupY[i], powerupZ[i]], 1, 1, 1, c.BLUE) + CASE 2: ' Shield - cyan + rl.drawSphere([powerupX[i], powerupY[i], powerupZ[i]], 0.8, c.SKYBLUE) + CASE 3: ' Weapon - yellow + rl.drawCube([powerupX[i], powerupY[i], powerupZ[i]], 0.7, 0.7, 0.7, c.GOLD) + END SELECT + + ' Add rotation effect + rl.drawCubeWires([powerupX[i], powerupY[i], powerupZ[i]], 1.2, 1.2, 1.2, c.WHITE) + ENDIF + NEXT + + ' Draw player with shield if active + IF playerShieldActive THEN + rl.drawSphere([playerX, playerY, playerZ], playerSize * 1.5, rl.colorToInt([0, 191, 255, 100])) + ENDIF + + ' Draw player ship with rotation + ' Main body + rl.drawCube([playerX, playerY, playerZ], playerSize * 0.8, playerSize * 0.4, playerSize * 2, c.GREEN) + ' Wings + rl.drawCube([playerX - playerSize, playerY, playerZ + playerSize * 0.5], playerSize * 0.8, playerSize * 0.1, playerSize * 0.5, c.DARKGREEN) + rl.drawCube([playerX + playerSize, playerY, playerZ + playerSize * 0.5], playerSize * 0.8, playerSize * 0.1, playerSize * 0.5, c.DARKGREEN) + + ' Engine glow + rl.drawSphere([playerX, playerY, playerZ + playerSize], playerSize * 0.3, rl.colorToInt([0, 100, 255, 200])) + + ' Draw bullets + FOR i = 0 TO MAX_BULLETS - 1 + IF bulletActive[i] THEN + IF bulletType[i] = 1 THEN + ' Upgraded bullets + rl.drawSphere([bulletX[i], bulletY[i], bulletZ[i]], 0.4, c.GOLD) + ELSE + ' Regular bullets + rl.drawSphere([bulletX[i], bulletY[i], bulletZ[i]], 0.3, c.YELLOW) + ENDIF + ENDIF + NEXT + + ' Draw enemies + FOR i = 0 TO MAX_ENEMIES - 1 + LET enemyColor = GetEnemyColor(enemyType[i], enemyHealth[i], IFF(enemyType[i] == 2, (10 + (level * 5)), IFF(enemyType[i] == 1, 1, 2))) + + ' Different enemy types have different shapes + SELECT CASE enemyType[i] + CASE 0: ' Regular enemy + rl.drawCube([enemyX[i], enemyY[i], enemyZ[i]], enemySize[i], enemySize[i], enemySize[i], enemyColor) + CASE 1: ' Fast enemy - triangular + rl.drawCube([enemyX[i], enemyY[i], enemyZ[i]], enemySize[i] * 0.6, enemySize[i] * 0.6, enemySize[i] * 1.5, enemyColor) + ' Wings for fast enemy + rl.drawCube([enemyX[i] - enemySize[i] * 0.7, enemyY[i], enemyZ[i]], enemySize[i] * 0.5, enemySize[i] * 0.1, enemySize[i] * 0.3, enemyColor) + rl.drawCube([enemyX[i] + enemySize[i] * 0.7, enemyY[i], enemyZ[i]], enemySize[i] * 0.5, enemySize[i] * 0.1, enemySize[i] * 0.3, enemyColor) + CASE 2: ' Boss - larger with complex shape + ' Main body + rl.drawSphere([enemyX[i], enemyY[i], enemyZ[i]], enemySize[i] * 0.8, enemyColor) + ' Top structure + rl.drawCube([enemyX[i], enemyY[i] + enemySize[i] * 0.6, enemyZ[i]], enemySize[i] * 0.5, enemySize[i] * 0.4, enemySize[i] * 0.5, enemyColor) + ' Side wings + rl.drawCube([enemyX[i] - enemySize[i], enemyY[i], enemyZ[i]], enemySize[i] * 0.8, enemySize[i] * 0.2, enemySize[i] * 0.6, enemyColor) + rl.drawCube([enemyX[i] + enemySize[i], enemyY[i], enemyZ[i]], enemySize[i] * 0.8, enemySize[i] * 0.2, enemySize[i] * 0.6, enemyColor) + ' Engines + rl.drawSphere([enemyX[i] - enemySize[i], enemyY[i], enemyZ[i] + enemySize[i] * 0.4], enemySize[i] * 0.2, rl.colorToInt([255, 100, 0, 255])) + rl.drawSphere([enemyX[i] + enemySize[i], enemyY[i], enemyZ[i] + enemySize[i] * 0.4], enemySize[i] * 0.2, rl.colorToInt([255, 100, 0, 255])) + END SELECT + + ' Draw health bar for enemies with more than 1 health + IF enemyHealth[i] > 1 THEN + DrawHealthBar(enemyX[i], enemyY[i], enemyZ[i], enemyHealth[i], IFF(enemyType[i] = 2, (10 + (level * 5)), 2), enemySize[i]) + ENDIF + NEXT + + rl.endMode3D() + + ' Draw UI + rl.drawText("SCORE: " + score, 10, 10, 20, c.YELLOW) + rl.drawText("LEVEL: " + level, 10, 40, 20, c.GREEN) + + ' Draw health bar + rl.drawRectangle(SCREEN_WIDTH - 210, 10, 200, 20, c.DARKGRAY) + + local rc_col = IFF((playerHealth > 60), c.GREEN, IFF((playerHealth > 30), c.YELLOW, c.RED)) + rl.drawRectangle(SCREEN_WIDTH - 210, 10, (playerHealth * 2), 20, rc_col) + +' rl.drawRectangle(SCREEN_WIDTH - 210, 10, (playerHealth * 2), 20, IFF((playerHealth > 60), c.GREEN, IFF((playerHealth > 30), c.YELLOW, c.RED))) + + + rl.drawText("HEALTH", SCREEN_WIDTH - 210, 35, 20, c.WHITE) + + ' Draw shield status + IF playerShieldActive THEN + rl.drawRectangle(SCREEN_WIDTH - 210, 60, playerShieldTimer * 200 / SHIELD_DURATION, 10, c.SKYBLUE) + rl.drawText("SHIELD ACTIVE", SCREEN_WIDTH - 210, 75, 20, c.SKYBLUE) + ELSEIF playerShieldCooldown > 0 THEN + rl.drawRectangle(SCREEN_WIDTH - 210, 60, 200 - (playerShieldCooldown * 200 / (SHIELD_DURATION + SHIELD_COOLDOWN)), 10, c.DARKBLUE) + rl.drawText("SHIELD CHARGING", SCREEN_WIDTH - 210, 75, 20, c.DARKBLUE) + ELSE + rl.drawText("SHIELD READY (S)", SCREEN_WIDTH - 210, 75, 20, c.BLUE) + ENDIF + + ' Draw weapon status + IF weaponUpgraded THEN + rl.drawRectangle(SCREEN_WIDTH - 210, 100, weaponUpgradeTimer * 200 / WEAPON_UPGRADE_DURATION, 10, c.GOLD) + rl.drawText("WEAPON UPGRADED", SCREEN_WIDTH - 210, 115, 20, c.GOLD) + ENDIF + + ' Draw boss info if in boss fight + IF bossFight AND bossIndex >= 0 THEN + rl.drawText("BOSS HEALTH:", SCREEN_WIDTH/2 - 200, SCREEN_HEIGHT - 40, 20, c.RED) + rl.drawRectangle(SCREEN_WIDTH/2, SCREEN_HEIGHT - 40, 200, 20, c.DARKGRAY) + rl.drawRectangle(SCREEN_WIDTH/2, SCREEN_HEIGHT - 40, enemyHealth[bossIndex] * 200 / (10 + (level * 5)), 20, c.RED) + ENDIF + + rl.endDrawing() + ELSE + ' Game over screen + rl.beginDrawing() + rl.clearBackground(c.BLACK) + + ' Draw stars in the background + FOR i = 0 TO MAX_STARS - 1 + LET starDepth = 1.0 - (starZ[i] / -200) + LET brightness = 100 + starDepth * 155 + rl.drawCircle(SCREEN_WIDTH/2 + starX[i], SCREEN_HEIGHT/2 + starY[i], starSize[i] * starDepth, rl.colorToInt([brightness, brightness, brightness, 255])) + starZ[i] += 0.2 + IF starZ[i] > 10 THEN + starZ[i] = -200 + starX[i] = (RND() - 0.5) * 100 + starY[i] = (RND() - 0.5) * 100 + ENDIF + NEXT + + rl.drawText("GAME OVER", SCREEN_WIDTH/2 - 120, SCREEN_HEIGHT/3, 40, c.RED) + rl.drawText("Final Score: " + score, SCREEN_WIDTH/2 - 100, SCREEN_HEIGHT/2 - 20, 30, c.YELLOW) + rl.drawText("Level Reached: " + level, SCREEN_WIDTH/2 - 100, SCREEN_HEIGHT/2 + 20, 20, c.GREEN) + + IF score > highScore THEN + rl.drawText("NEW HIGH SCORE!", SCREEN_WIDTH/2 - 120, SCREEN_HEIGHT/2 + 60, 25, c.GOLD) + ELSE + rl.drawText("High Score: " + highScore, SCREEN_WIDTH/2 - 100, SCREEN_HEIGHT/2 + 60, 25, c.WHITE) + ENDIF + + rl.drawText("Press R to restart", SCREEN_WIDTH/2 - 100, SCREEN_HEIGHT/2 + 120, 20, c.WHITE) + rl.drawText("Press ESC to quit", SCREEN_WIDTH/2 - 100, SCREEN_HEIGHT/2 + 150, 20, c.WHITE) + + rl.endDrawing() + + ' Check for restart + IF rl.isKeyPressed(c.KEY_R) THEN + StartNewGame() + ELSEIF rl.isKeyPressed(c.KEY_ESCAPE) THEN + EXIT LOOP + ENDIF + ENDIF +WEND +rl.closeWindow() + From b49bd8f432844ef5698b5ebee4f99ea5e808f608 Mon Sep 17 00:00:00 2001 From: chrisws Date: Wed, 2 Jul 2025 20:38:22 +0930 Subject: [PATCH 060/131] update build setting for play store compliance. --- ioio/ioio/Android.mk | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ioio/ioio/Android.mk b/ioio/ioio/Android.mk index 5090fcb..537612e 100644 --- a/ioio/ioio/Android.mk +++ b/ioio/ioio/Android.mk @@ -16,5 +16,7 @@ LOCAL_SRC_FILES := ../../include/param.cpp \ ../../include/apiexec.cpp \ ../main.cpp LOCAL_LDLIBS := -llog -landroid +LOCAL_LDFLAGS += "-Wl,-z,max-page-size=16384" +LOCAL_LDFLAGS += "-Wl,-z,common-page-size=16384" include $(BUILD_SHARED_LIBRARY) From e784f286799dd18bdbf1d3ec3c341b7738832a06 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Sun, 16 Nov 2025 20:32:26 +1030 Subject: [PATCH 061/131] RAYLIB: update to new rlparser location --- raylib/Makefile.am | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/raylib/Makefile.am b/raylib/Makefile.am index 7db06af..b67153e 100644 --- a/raylib/Makefile.am +++ b/raylib/Makefile.am @@ -10,8 +10,8 @@ sbasic=sbasic CLEANFILES = $(generated) -raylib/parser/raylib_api.json: raylib/src/raylib.h raylib/parser/raylib_parser.c - (cd raylib/parser && make && ./raylib_parser --format JSON --input ../src/raylib.h --output raylib_api.json) +raylib/tools/rlparser/output/raylib_api.json: raylib/src/raylib.h raylib/tools/rlparser/rlparser.c + (cd raylib/tools/rlparser && make && ./rlparser --format JSON --input ../../src/raylib.h --output raylib_api.json) UNSUPPORTED.md: $(generated) $(sbasic) mkraylib.bas unsupported > $@ From b8470329fd523f7dd76b8c4eefd968b91ca61c03 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Mon, 17 Nov 2025 18:45:32 +1030 Subject: [PATCH 062/131] RAYLIB: depending on sbasic doesn't work with flatpak builds --- configure.ac | 4 ---- 1 file changed, 4 deletions(-) diff --git a/configure.ac b/configure.ac index 2c7512a..a9f12a5 100644 --- a/configure.ac +++ b/configure.ac @@ -17,10 +17,6 @@ AC_LANG([C++]) AM_INIT_AUTOMAKE([subdir-objects]) LT_INIT([win32-dll]) -dnl sbasic is required for raylib code generation -AC_CHECK_PROG(SBASIC_CHECK,sbasic, yes) -AS_IF([test x"$SBASIC_CHECK" != x"yes"], [AC_MSG_ERROR([Please install sbasic before configuring.])]) - function checkDebugMode() { AC_MSG_CHECKING([if debug mode is enabled]) AC_ARG_WITH(debug, From 53939fd6953aab34cc3b7bd47a726a57cbd43b77 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Mon, 17 Nov 2025 20:54:48 +1030 Subject: [PATCH 063/131] RAYLIB: depending on sbasic doesn't work with flatpak builds --- raylib/Makefile.am | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/raylib/Makefile.am b/raylib/Makefile.am index b67153e..60b17a4 100644 --- a/raylib/Makefile.am +++ b/raylib/Makefile.am @@ -8,8 +8,6 @@ generated = func-def.h proc-def.h proc.h func.h sbasic=sbasic -CLEANFILES = $(generated) - raylib/tools/rlparser/output/raylib_api.json: raylib/src/raylib.h raylib/tools/rlparser/rlparser.c (cd raylib/tools/rlparser && make && ./rlparser --format JSON --input ../../src/raylib.h --output raylib_api.json) @@ -23,9 +21,7 @@ $(generated): raylib/parser/raylib_api.json mkraylib.bas $(sbasic) mkraylib.bas $@ > $@ @touch main.cpp -gen: $(generated) - -all-am: $(generated) README.md +gen: $(generated) README.md AM_CXXFLAGS=-fno-rtti -std=c++14 -fpermissive AM_CPPFLAGS = -Iraylib/src -Iraylib/src/external/glfw/include -Iraylib/src/external/glfw/deps/mingw \ From 9c25099579df2c5b7053ab1daf524987bdf40eb9 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Wed, 3 Dec 2025 20:29:24 +1030 Subject: [PATCH 064/131] Build settings for inclusion in flatpak build --- configure.ac | 2 +- raylib/Makefile.am | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/configure.ac b/configure.ac index a9f12a5..f3a23f0 100644 --- a/configure.ac +++ b/configure.ac @@ -70,7 +70,7 @@ case "${host_os}" in WEBSOCKET_LDFLAGS="" GTK_SERVER_LDFLAGS="`pkg-config --libs gtk+-3.0` -lXm -lXt" GTK_SERVER_CPPFLAGS="`pkg-config --cflags gtk+-3.0` -DGTK_SERVER_FFI -DGTK_SERVER_LIBRARY -DGTK_SERVER_UNIX -DGTK_SERVER_GTK3x" - RAYLIB_LDFLAGS="-lGL -lm -lpthread -ldl -lrt -lX11" + RAYLIB_LDFLAGS="-lGL -lm -lpthread -ldl -lrt -lX11 -lwayland-client -lwayland-cursor -lwayland-egl -lxkbcommon" JVM_CPPFLAGS="-I/usr/lib/jvm/java-1.8.0-openjdk-amd64/include -I/usr/lib/jvm/java-1.8.0-openjdk-amd64/include/linux" JVM_LDFLAGS="-L/usr/lib/jvm/java-1.8.0-openjdk-amd64/jre/lib/amd64/server -ljvm" NUKLEAR_CPPFLAGS="-D_GLFW_X11=1" diff --git a/raylib/Makefile.am b/raylib/Makefile.am index 60b17a4..875d5dc 100644 --- a/raylib/Makefile.am +++ b/raylib/Makefile.am @@ -25,8 +25,9 @@ gen: $(generated) README.md AM_CXXFLAGS=-fno-rtti -std=c++14 -fpermissive AM_CPPFLAGS = -Iraylib/src -Iraylib/src/external/glfw/include -Iraylib/src/external/glfw/deps/mingw \ + -Iraylib/src/external/glfw/src \ -DPLATFORM_DESKTOP=1 -DSUPPORT_BUSY_WAIT_LOOP=1 -DSUPPORT_SCREEN_CAPTURE=1 \ - -DSUPPORT_GIF_RECORDING=1 -DSUPPORT_COMPRESSION_API=1 -D_GLFW_BUILD_DLL=1 \ + -DSUPPORT_GIF_RECORDING=1 -DSUPPORT_COMPRESSION_API=1 -D_GLFW_WAYLAND=1 \ -Wall -Wextra -Wshadow -Wdouble-promotion -Wno-unused-parameter -fPIC lib_LTLIBRARIES = libraylib.la From a9abd5ff62fffb6fb51b434d03c31f84d814cf23 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Sat, 13 Dec 2025 07:42:10 +1030 Subject: [PATCH 065/131] LLM: plugin module - initial commit --- .gitmodules | 3 ++ llama/CMakeLists.txt | 113 +++++++++++++++++++++++++++++++++++++++++++ llama/llama.cpp | 1 + llama/main.cpp | 74 ++++++++++++++++++++++++++++ llama/test_main.cpp | 112 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 303 insertions(+) create mode 100644 llama/CMakeLists.txt create mode 160000 llama/llama.cpp create mode 100644 llama/main.cpp create mode 100644 llama/test_main.cpp diff --git a/.gitmodules b/.gitmodules index 234ac82..ed8d662 100644 --- a/.gitmodules +++ b/.gitmodules @@ -31,3 +31,6 @@ [submodule "gtk-server/uthash"] path = gtk-server/uthash url = https://github.com/troydhanson/uthash.git +[submodule "llama/llama.cpp"] + path = llama/llama.cpp + url = https://github.com/ggerganov/llama.cpp diff --git a/llama/CMakeLists.txt b/llama/CMakeLists.txt new file mode 100644 index 0000000..6f173fb --- /dev/null +++ b/llama/CMakeLists.txt @@ -0,0 +1,113 @@ +cmake_minimum_required(VERSION 3.15) +project(llm_plugin C CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_C_STANDARD 11) + +# ----------------------------- +# Path to llama.cpp +# ----------------------------- +set(LLAMA_DIR ${CMAKE_CURRENT_SOURCE_DIR}/llama.cpp) + +# ----------------------------- +# FORCE CPU-only static builds +# ----------------------------- +# Disable all shared libraries globally +set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) + +# llama.cpp specific static settings +set(LLAMA_STATIC ON CACHE BOOL "" FORCE) +set(LLAMA_SHARED OFF CACHE BOOL "" FORCE) +set(LLAMA_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) +set(LLAMA_BUILD_LLAMA_SHARED OFF CACHE BOOL "" FORCE) +set(LLAMA_BUILD_GGML_SHARED OFF CACHE BOOL "" FORCE) +set(LLAMA_SERVER_BUILD OFF CACHE BOOL "" FORCE) +set(LLAMA_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(LLAMA_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) + +# ggml specific static settings +set(GGML_STATIC ON CACHE BOOL "" FORCE) +set(GGML_SHARED OFF CACHE BOOL "" FORCE) +set(GGML_BUILD_SHARED OFF CACHE BOOL "" FORCE) +set(GGML_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(GGML_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) + +# CPU-only flags +set(GGML_CUDA OFF CACHE BOOL "" FORCE) +set(GGML_METAL OFF CACHE BOOL "" FORCE) +set(GGML_OPENCL OFF CACHE BOOL "" FORCE) +set(GGML_KOMPUTE OFF CACHE BOOL "" FORCE) +set(GGML_SYCL OFF CACHE BOOL "" FORCE) +set(GGML_ACCELERATE OFF CACHE BOOL "" FORCE) +set(GGML_NATIVE ON CACHE BOOL "" FORCE) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +# ----------------------------- +# Add llama.cpp subdirectories +# ----------------------------- +add_subdirectory(${LLAMA_DIR}/ggml) +add_subdirectory(${LLAMA_DIR}) + +# ----------------------------- +# Plugin sources +# ----------------------------- +set(PLUGIN_SOURCES + main.cpp + ../include/param.cpp + ../include/hashmap.cpp + ../include/apiexec.cpp +) + +# ----------------------------- +# Build plugin as a shared library (.so) +# ----------------------------- +add_library(llm_plugin SHARED ${PLUGIN_SOURCES}) + +target_include_directories(llm_plugin PRIVATE + ${LLAMA_DIR}/include + ${LLAMA_DIR}/ggml/include + ${CMAKE_CURRENT_SOURCE_DIR}/../include + ${CMAKE_CURRENT_SOURCE_DIR}/.. +) + +target_link_libraries(llm_plugin PRIVATE + llama + ggml +) + +# Include all static code into plugin +target_link_options(llm_plugin PRIVATE + -Wl,--whole-archive + $ + $ + -Wl,--no-whole-archive +) + +# Ensure position-independent code for .so +set_target_properties(llm_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib +) + +# ----------------------------- +# Optional test application +# ----------------------------- +add_executable(llm_test + test_main.cpp +) + +target_include_directories(llm_test PRIVATE + ${LLAMA_DIR}/include + ${LLAMA_DIR}/ggml/include + ${CMAKE_CURRENT_SOURCE_DIR}/../include +) + +target_link_libraries(llm_test PRIVATE + llm_plugin + llama + ggml +) + +set_target_properties(llm_test PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin +) diff --git a/llama/llama.cpp b/llama/llama.cpp new file mode 160000 index 0000000..e4ae383 --- /dev/null +++ b/llama/llama.cpp @@ -0,0 +1 @@ +Subproject commit e4ae38331702aeb43b6ecc3f912d626171c9862a diff --git a/llama/main.cpp b/llama/main.cpp new file mode 100644 index 0000000..43403ee --- /dev/null +++ b/llama/main.cpp @@ -0,0 +1,74 @@ +// This file is part of SmallBASIC +// +// This program is distributed under the terms of the GPL v2.0 or later +// Download the GNU Public License (GPL) from www.gnu.org +// +// Copyright(C) 2026 Chris Warren-Smith + +#include "config.h" + +#include +#include +#include "robin-hood-hashing/src/include/robin_hood.h" +#include "include/log.h" +#include "include/var.h" +#include "include/module.h" +#include "include/param.h" + +#define CLASS_IOTASK_ID 1 + +int g_nextId = 1; + +FUNC_SIG lib_func[] = { +}; + +FUNC_SIG lib_proc[] = { +}; + +SBLIB_API int sblib_proc_count() { + return 0; +} + +SBLIB_API int sblib_func_count() { + return 0; +} + +// +// Program startup +// +int sblib_init(const char *sourceFile) { + return 1; +} + +#if defined(ANDROID_MODULE) +// +// Retrieves the _app->activity->clazz value sent from App/JNI to Java to IOIOLoader +// +extern "C" JNIEXPORT void JNICALL Java_ioio_smallbasic_android_ModuleLoader_init + (JNIEnv *env, jclass clazz, jobject activity) { + logEntered(); + jclass longClass = env->FindClass("java/lang/Long"); + jmethodID longValueMethod = env->GetMethodID(longClass, "longValue", "()J"); + g_activity = (jobject)env->CallLongMethod(activity, longValueMethod); + g_env = env; +} + +#endif + +// +// Release ioio variables falling out of scope +// +SBLIB_API void sblib_free(int cls_id, int id) { + if (id != -1) { + switch (cls_id) { + case CLASS_IOTASK_ID: + break; + } + } +} + +// +// Program termination +// +void sblib_close(void) { +} diff --git a/llama/test_main.cpp b/llama/test_main.cpp new file mode 100644 index 0000000..80d5bab --- /dev/null +++ b/llama/test_main.cpp @@ -0,0 +1,112 @@ +#include +#include +#include +#include + +#define LLAMA_BUILD_EXAMPLES +#include "llama.h" + +int main() { + const char *model_path = "model.gguf"; + + // --- Model load --- + llama_model_params model_params = llama_model_default_params(); + llama_model *model = llama_model_load_from_file(model_path, model_params); + if (!model) { + fprintf(stderr, "failed to load model\n"); + return 1; + } + + const llama_vocab *vocab = llama_model_get_vocab(model); + + // --- Context creation --- + llama_context_params ctx_params = llama_context_default_params(); + llama_context *ctx = llama_init_from_model(model, ctx_params); + if (!ctx) { + fprintf(stderr, "failed to create context\n"); + llama_model_free(model); + return 1; + } + + // --- Tokenize --- + std::string prompt = "Hello, Llama!"; + + int n_tokens = llama_tokenize( + vocab, + prompt.c_str(), + prompt.size(), + nullptr, + 0, + true, // add BOS + false + ); + + if (n_tokens <= 0) return 1; + + std::vector tokens(n_tokens); + + llama_tokenize( + vocab, + prompt.c_str(), + prompt.size(), + tokens.data(), + n_tokens, + true, + false + ); + + // --- Build batch for prompt --- + llama_batch batch = llama_batch_init(tokens.size(), 0, 1); + + for (size_t i = 0; i < tokens.size(); i++) { + batch.token[i] = tokens[i]; + batch.pos[i] = i; + batch.seq_id[i] = 0; + batch.logits[i] = false; + batch.n_tokens++; + } + + if (llama_decode(ctx, batch) != 0) { + fprintf(stderr, "decode failed\n"); + return 1; + } + + // --- Sampler --- + llama_sampler *sampler = llama_sampler_init_greedy(); + + // --- Generation loop --- + for (int i = 0; i < 100; i++) { + llama_token tok = llama_sampler_sample(sampler, ctx, 0); + + //std::string piece = llama_vocab_token_to_piece(vocab, tok); + char piece_buf[512]; + int32_t piece_len = llama_token_to_piece( + vocab, + tok, + piece_buf, + sizeof(piece_buf), + 0, // lstrip + true // print special tokens + ); + + if (piece_len > 0) { + printf("%s", piece_buf); + } + + // feed token back + llama_batch next = llama_batch_init(1, 0, 1); + next.token[0] = tok; + next.pos[0] = 0; + next.seq_id[0]= 0; + next.logits[0]= false; + next.n_tokens = 1; + + if (llama_decode(ctx, next) != 0) break; + } + + printf("\n"); + + llama_sampler_free(sampler); + llama_free(ctx); + llama_model_free(model); +} From 50c5c2ae1e6c6d0e310051f9f97fa0f9ae7db9e6 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Tue, 16 Dec 2025 10:34:48 +1030 Subject: [PATCH 066/131] LLM: plugin module - initial commit --- llama/CMakeLists.txt | 54 ++++++--- llama/llama-sb.cpp | 145 +++++++++++++++++++++++ llama/llama-sb.h | 39 +++++++ llama/llama.cpp | 2 +- llama/main.cpp | 230 +++++++++++++++++++++++++++++++++---- llama/test_main.cpp | 265 +++++++++++++++++++++++++++++-------------- 6 files changed, 611 insertions(+), 124 deletions(-) create mode 100644 llama/llama-sb.cpp create mode 100644 llama/llama-sb.h diff --git a/llama/CMakeLists.txt b/llama/CMakeLists.txt index 6f173fb..23f41b9 100644 --- a/llama/CMakeLists.txt +++ b/llama/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.15) -project(llm_plugin C CXX) +project(llm C CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_C_STANDARD 11) @@ -49,42 +49,40 @@ add_subdirectory(${LLAMA_DIR}/ggml) add_subdirectory(${LLAMA_DIR}) # ----------------------------- -# Plugin sources +# Build plugin as a shared library (.so) # ----------------------------- set(PLUGIN_SOURCES main.cpp + llama-sb.cpp ../include/param.cpp ../include/hashmap.cpp ../include/apiexec.cpp ) -# ----------------------------- -# Build plugin as a shared library (.so) -# ----------------------------- -add_library(llm_plugin SHARED ${PLUGIN_SOURCES}) +add_library(llm SHARED ${PLUGIN_SOURCES}) -target_include_directories(llm_plugin PRIVATE +target_include_directories(llm PRIVATE ${LLAMA_DIR}/include ${LLAMA_DIR}/ggml/include ${CMAKE_CURRENT_SOURCE_DIR}/../include ${CMAKE_CURRENT_SOURCE_DIR}/.. ) -target_link_libraries(llm_plugin PRIVATE +target_link_libraries(llm PRIVATE llama ggml ) # Include all static code into plugin -target_link_options(llm_plugin PRIVATE +target_link_options(llm PRIVATE -Wl,--whole-archive - $ - $ + $ + $ -Wl,--no-whole-archive ) # Ensure position-independent code for .so -set_target_properties(llm_plugin PROPERTIES +set_target_properties(llm PROPERTIES POSITION_INDEPENDENT_CODE ON LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib ) @@ -103,7 +101,7 @@ target_include_directories(llm_test PRIVATE ) target_link_libraries(llm_test PRIVATE - llm_plugin + llm llama ggml ) @@ -111,3 +109,33 @@ target_link_libraries(llm_test PRIVATE set_target_properties(llm_test PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin ) + +# ------------------------------------------------------------------ +# Android native library +# ------------------------------------------------------------------ +if (ANDROID) + # CMake sets ANDROID when using the Android toolchain + # Re‑use the same source files for the Android .so + add_library(llm_android SHARED + main.cpp + llama-sb.cpp + ../include/param.cpp + ../include/hashmap.cpp + ../include/apiexec.cpp + ) + + # Optional: set the SONAME / versioning if you need it + set_target_properties(llm_android PROPERTIES + OUTPUT_NAME "libllm" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${ANDROID_ABI}") + + target_link_libraries(llm_test PRIVATE + log + llm + llama + ggml + ) + + # Export the location so Gradle can copy it later + set(MY_NATIVE_LIB_PATH "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${ANDROID_ABI}/libllm.so") +endif() diff --git a/llama/llama-sb.cpp b/llama/llama-sb.cpp new file mode 100644 index 0000000..1e70a52 --- /dev/null +++ b/llama/llama-sb.cpp @@ -0,0 +1,145 @@ +// This file is part of SmallBASIC +// +// This program is distributed under the terms of the GPL v2.0 or later +// Download the GNU Public License (GPL) from www.gnu.org +// +// Copyright(C) 2026 Chris Warren-Smith + +#include +#include +#include +#include + +#include "llama.h" +#include "llama-sb.h" + +Llama::Llama() : + _model(nullptr), + _ctx(nullptr), + _sampler(nullptr), + _vocab(nullptr), + _temperature(0), + _n_ctx(0) { +} + +bool Llama::create(string model_path, int n_ctx, bool disable_log) { + if (disable_log) { + // only print errors + llama_log_set([](enum ggml_log_level level, const char * text, void * /* user_data */) { + if (level >= GGML_LOG_LEVEL_ERROR) { + fprintf(stderr, "%s", text); + } + }, nullptr); + } + + ggml_backend_load_all(); + + llama_model_params mparams = llama_model_default_params(); + mparams.n_gpu_layers = 0; + + _model = llama_model_load_from_file(model_path.c_str(), mparams); + if (!_model) { + _last_error = "failed to load model"; + } else { + llama_context_params cparams = llama_context_default_params(); + cparams.n_ctx = n_ctx; + cparams.n_batch = n_ctx; + + _ctx = llama_init_from_model(_model, cparams); + if (!_ctx) { + _last_error = "failed to create context"; + } else { + _vocab = llama_model_get_vocab(_model); + configure_sampler(0); + } + } + return _last_error.empty(); +} + +Llama::~Llama() { + if (_sampler) { + llama_sampler_free(_sampler); + } + if (_ctx) { + llama_free(_ctx); + } + if (_model) { + llama_model_free(_model); + } +} + +string Llama::build_chat_prompt(const string &user_msg) { + _chat_prompt += "User: "; + _chat_prompt += user_msg; + _chat_prompt += "\nAssistant: "; + return _chat_prompt; +} + +void Llama::configure_sampler(float temperature) { + if (temperature != _temperature || _sampler == nullptr) { + if (_sampler) { + llama_sampler_free(_sampler); + } + auto sparams = llama_sampler_chain_default_params(); + _sampler = llama_sampler_chain_init(sparams); + _temperature = temperature; + + // llama_sampler_chain_reset(sampler); + if (temperature <= 0.0f) { + llama_sampler_chain_add(_sampler, llama_sampler_init_greedy()); + } else { + llama_sampler_chain_add(_sampler, llama_sampler_init_temp(temperature)); + } + } +} + +static std::vector tokenize(const llama_vocab *vocab, const string &text) { + int n = -llama_tokenize(vocab, text.c_str(), text.size(), nullptr, 0, true, true); + std::vector tokens(n); + llama_tokenize(vocab, text.c_str(), text.size(), tokens.data(), tokens.size(), true, true); + return tokens; +} + +string Llama::generate(const string &prompt, int max_tokens, float temperature, bool echo, bool clear_cache) { + string out; + + if (clear_cache) { + // llama_kv_cache_clear(_ctx); + } + + auto prompt_tokens = tokenize(_vocab, prompt); + configure_sampler(temperature); + + llama_batch batch = llama_batch_get_one(prompt_tokens.data(), prompt_tokens.size()); + + if (llama_decode(_ctx, batch)) { + _last_error = "decode failed"; + return out; + } + + if (echo) { + out += prompt; + } + + for (int i = 0; i < max_tokens; ++i) { + llama_token tok = llama_sampler_sample(_sampler, _ctx, -1); + + if (llama_vocab_is_eog(_vocab, tok)) { + break; + } + + char buf[128]; + int n = llama_token_to_piece(_vocab, tok, buf, sizeof(buf), 0, true); + + if (n > 0) { + out.append(buf, n); + } + batch = llama_batch_get_one(&tok, 1); + if (llama_decode(_ctx, batch)) { + break; + } + } + + return out; +} + diff --git a/llama/llama-sb.h b/llama/llama-sb.h new file mode 100644 index 0000000..2d4d216 --- /dev/null +++ b/llama/llama-sb.h @@ -0,0 +1,39 @@ +// This file is part of SmallBASIC +// +// This program is distributed under the terms of the GPL v2.0 or later +// Download the GNU Public License (GPL) from www.gnu.org +// +// Copyright(C) 2026 Chris Warren-Smith + +#pragma once + +#include +#include "llama.h" + +using namespace std; + +struct Llama { + explicit Llama(); + ~Llama(); + + bool create(string model_path, int n_ctx, bool disable_log); + string generate(const string &prompt, + int max_tokens = 128, + float temperature = 0.8f, + bool echo = true, + bool clear_cache = true); + const char *last_error() { return _last_error.c_str(); } + + private: + string build_chat_prompt(const string &user_msg); + void configure_sampler(float temperature); + + llama_model *_model; + llama_context *_ctx; + llama_sampler *_sampler; + const llama_vocab *_vocab; + string _chat_prompt; + string _last_error; + float _temperature; + int _n_ctx; +}; diff --git a/llama/llama.cpp b/llama/llama.cpp index e4ae383..380b4c9 160000 --- a/llama/llama.cpp +++ b/llama/llama.cpp @@ -1 +1 @@ -Subproject commit e4ae38331702aeb43b6ecc3f912d626171c9862a +Subproject commit 380b4c984e06f8d8381392d15814b3392e39560e diff --git a/llama/main.cpp b/llama/main.cpp index 43403ee..eff3197 100644 --- a/llama/main.cpp +++ b/llama/main.cpp @@ -7,29 +7,202 @@ #include "config.h" -#include -#include #include "robin-hood-hashing/src/include/robin_hood.h" #include "include/log.h" #include "include/var.h" #include "include/module.h" #include "include/param.h" -#define CLASS_IOTASK_ID 1 +#include "llama-sb.h" + +#define CLASS_ID 1 int g_nextId = 1; +robin_hood::unordered_map g_map; -FUNC_SIG lib_func[] = { -}; +static int get_id(var_s *map, var_s *retval) { + int result = -1; + if (is_map(map)) { + int id = map->v.m.id; + if (id != -1 && g_map.find(id) != g_map.end()) { + result = id; + } + } + if (result == -1) { + error(retval, "Llama not found"); + } + return result; +} -FUNC_SIG lib_proc[] = { -}; +const char *llm_llama_chat(int id, + const char * user_message, + int max_tokens, + float temperature) { + static thread_local string result; + + // auto it = g_llamas.find(h); + // if (it == g_llamas.end()) { + // result = "[invalid llama handle]"; + // return result.c_str(); + // } + + // Llama * llm = it->second.get(); + + // // build accumulated prompt + // string prompt = build_chat_prompt(llm, user_message); + + // // run generation WITHOUT clearing cache + // result = llm->generate(prompt, + // max_tokens, + // temperature, + // false); // echo = false + + // // append assistant reply to history + // llm->chat_prompt += result; + // llm->chat_prompt += "\n"; + + return result.c_str(); +} + +// +// make the model forget everything +// +void llm_llama_reset() { + // std::lock_guard lock(g_mutex); + + // auto it = g_llamas.find(h); + // if (it == g_llamas.end()) return; + + // llama_kv_cache_clear(it->second->ctx); + // it->second->chat_prompt.clear(); +} + +// +// string generate(prompt, max_tokens, temperature) +// +const char *llm_llama_generate(const char * prompt, + int max_tokens, + float temperature) { + // static thread_local string result; + + // std::lock_guard lock(g_mutex); + + // auto it = g_llamas.find(h); + // if (it == g_llamas.end()) { + // result = "[invalid llama handle]"; + // return result.c_str(); + // } + + // try { + // result = it->second->generate(prompt, + // max_tokens, + // temperature); + // } catch (const std::exception & e) { + // result = e.what(); + // } + + // return result.c_str(); + return nullptr; +} + + +static int llm_llama_create(const char *model_path, int n_ctx) { + // std::lock_guard lock(g_mutex); + + // llama_handle id = g_next_id++; + + // try { + // g_llamas[id] = std::make_unique(model_path, n_ctx); + // } catch (...) { + // return 0; + // } -SBLIB_API int sblib_proc_count() { return 0; } +void llm_llama_destroy() { + // std::lock_guard lock(g_mutex); + // g_llamas.erase(h); +} + +// const char *llm_llama_generate(llama_handle h, +// const char *prompt, +// int max_tokens, +// float temperature) { +// static thread_local string result; + +// auto it = g_llamas.find(h); +// if (it == g_llamas.end()) { +// result = "[invalid llama handle]"; +// return result.c_str(); +// } + +// try { +// result = it->second->generate(prompt, max_tokens, temperature); +// } catch (const std::exception &e) { +// result = e.what(); +// } + +// return result.c_str(); +// } + +string expand_path(const char *path) { + string result; + if (path && path[0] == '~') { + const char *home = getenv("HOME"); + if (home != nullptr) { + result.append(home); + result.append(path + 1); + } else { + result = path; + } + } else { + result = path; + } + return result; +} + +static int cmd_create_llama(int argc, slib_par_t *params, var_t *retval) { + int result; + auto model = expand_path(get_param_str(argc, params, 0, "")); + int n_ctx = get_param_int(argc, params, 0, 2048); + int disable_log = get_param_int(argc, params, 0, 1); + int id = ++g_nextId; + Llama &llama = g_map[id]; + if (llama.create(model, n_ctx, disable_log)) { + map_init_id(retval, id, CLASS_ID); + + // v_create_callback(map, "getVoltage", cmd_analoginput_getvoltage); + // v_create_callback(map, "getVoltageSync", cmd_analoginput_getvoltagesync); + // v_create_callback(map, "getReference", cmd_analoginput_getreference); + // v_create_callback(map, "read", cmd_analoginput_read); + // v_create_callback(map, "readSync", cmd_analoginput_readsync); + // v_create_callback(map, "getOverflowCount", cmd_analoginput_getoverflowcount); + // v_create_callback(map, "available", cmd_analoginput_available); + // v_create_callback(map, "readBuffered", cmd_analoginput_readbuffered); + // v_create_callback(map, "getVoltageBuffered", cmd_analoginput_getvoltagebuffered); + // v_create_callback(map, "getSampleRate", cmd_analoginput_getsamplerate); + + result = 1; + } else { + error(retval, llama.last_error()); + g_map.erase(id); + result = 0; + } + return result; +} + +FUNC_SIG lib_func[] = { + {1, 3, "LLAMA", cmd_create_llama}, +}; + SBLIB_API int sblib_func_count() { + return 1; +} + +FUNC_SIG lib_proc[] = {}; + +SBLIB_API int sblib_proc_count() { return 0; } @@ -40,28 +213,16 @@ int sblib_init(const char *sourceFile) { return 1; } -#if defined(ANDROID_MODULE) -// -// Retrieves the _app->activity->clazz value sent from App/JNI to Java to IOIOLoader -// -extern "C" JNIEXPORT void JNICALL Java_ioio_smallbasic_android_ModuleLoader_init - (JNIEnv *env, jclass clazz, jobject activity) { - logEntered(); - jclass longClass = env->FindClass("java/lang/Long"); - jmethodID longValueMethod = env->GetMethodID(longClass, "longValue", "()J"); - g_activity = (jobject)env->CallLongMethod(activity, longValueMethod); - g_env = env; -} - -#endif - // -// Release ioio variables falling out of scope +// Release variables falling out of scope // SBLIB_API void sblib_free(int cls_id, int id) { if (id != -1) { switch (cls_id) { - case CLASS_IOTASK_ID: + case CLASS_ID: + if (g_map.find(id) != g_map.end()) { + g_map.erase(id); + } break; } } @@ -71,4 +232,23 @@ SBLIB_API void sblib_free(int cls_id, int id) { // Program termination // void sblib_close(void) { + if (!g_map.empty()) { + fprintf(stderr, "LLM leak detected\n"); + g_map.clear(); + } +} + +#if defined(ANDROID_MODULE) +// +// Retrieves the _app->activity->clazz value sent from App/JNI to Java to IOIOLoader +// +extern "C" JNIEXPORT void JNICALL Java_ioio_smallbasic_android_ModuleLoader_init + (JNIEnv *env, jclass clazz, jobject activity) { + logEntered(); + jclass longClass = env->FindClass("java/lang/Long"); + jmethodID longValueMethod = env->GetMethodID(longClass, "longValue", "()J"); + g_activity = (jobject)env->CallLongMethod(activity, longValueMethod); + g_env = env; } + +#endif diff --git a/llama/test_main.cpp b/llama/test_main.cpp index 80d5bab..b25cde5 100644 --- a/llama/test_main.cpp +++ b/llama/test_main.cpp @@ -1,112 +1,207 @@ +#include "llama.h" #include -#include +#include +#include #include #include -#define LLAMA_BUILD_EXAMPLES -#include "llama.h" +static void print_usage(int, char ** argv) { + printf("\nexample usage:\n"); + printf("\n %s -m model.gguf [-c context_size] [-ngl n_gpu_layers]\n", argv[0]); + printf("\n"); +} -int main() { - const char *model_path = "model.gguf"; +int main(int argc, char ** argv) { + std::string model_path; + int ngl = 99; + int n_ctx = 2048; + + // parse command line arguments + for (int i = 1; i < argc; i++) { + try { + if (strcmp(argv[i], "-m") == 0) { + if (i + 1 < argc) { + model_path = argv[++i]; + } else { + print_usage(argc, argv); + return 1; + } + } else if (strcmp(argv[i], "-c") == 0) { + if (i + 1 < argc) { + n_ctx = std::stoi(argv[++i]); + } else { + print_usage(argc, argv); + return 1; + } + } else if (strcmp(argv[i], "-ngl") == 0) { + if (i + 1 < argc) { + ngl = std::stoi(argv[++i]); + } else { + print_usage(argc, argv); + return 1; + } + } else { + print_usage(argc, argv); + return 1; + } + } catch (std::exception & e) { + fprintf(stderr, "error: %s\n", e.what()); + print_usage(argc, argv); + return 1; + } + } + if (model_path.empty()) { + print_usage(argc, argv); + return 1; + } - // --- Model load --- + // only print errors + llama_log_set([](enum ggml_log_level level, const char * text, void * /* user_data */) { + if (level >= GGML_LOG_LEVEL_ERROR) { + fprintf(stderr, "%s", text); + } + }, nullptr); + + // load dynamic backends + ggml_backend_load_all(); + + // initialize the model llama_model_params model_params = llama_model_default_params(); - llama_model *model = llama_model_load_from_file(model_path, model_params); + model_params.n_gpu_layers = ngl; + + llama_model * model = llama_model_load_from_file(model_path.c_str(), model_params); if (!model) { - fprintf(stderr, "failed to load model\n"); + fprintf(stderr , "%s: error: unable to load model\n" , __func__); return 1; } - const llama_vocab *vocab = llama_model_get_vocab(model); + const llama_vocab * vocab = llama_model_get_vocab(model); - // --- Context creation --- + // initialize the context llama_context_params ctx_params = llama_context_default_params(); - llama_context *ctx = llama_init_from_model(model, ctx_params); + ctx_params.n_ctx = n_ctx; + ctx_params.n_batch = n_ctx; + + llama_context * ctx = llama_init_from_model(model, ctx_params); if (!ctx) { - fprintf(stderr, "failed to create context\n"); - llama_model_free(model); + fprintf(stderr , "%s: error: failed to create the llama_context\n" , __func__); return 1; } - // --- Tokenize --- - std::string prompt = "Hello, Llama!"; - - int n_tokens = llama_tokenize( - vocab, - prompt.c_str(), - prompt.size(), - nullptr, - 0, - true, // add BOS - false - ); - - if (n_tokens <= 0) return 1; - - std::vector tokens(n_tokens); - - llama_tokenize( - vocab, - prompt.c_str(), - prompt.size(), - tokens.data(), - n_tokens, - true, - false - ); - - // --- Build batch for prompt --- - llama_batch batch = llama_batch_init(tokens.size(), 0, 1); - - for (size_t i = 0; i < tokens.size(); i++) { - batch.token[i] = tokens[i]; - batch.pos[i] = i; - batch.seq_id[i] = 0; - batch.logits[i] = false; - batch.n_tokens++; - } + // initialize the sampler + llama_sampler * smpl = llama_sampler_chain_init(llama_sampler_chain_default_params()); + llama_sampler_chain_add(smpl, llama_sampler_init_min_p(0.05f, 1)); + llama_sampler_chain_add(smpl, llama_sampler_init_temp(0.8f)); + llama_sampler_chain_add(smpl, llama_sampler_init_dist(LLAMA_DEFAULT_SEED)); - if (llama_decode(ctx, batch) != 0) { - fprintf(stderr, "decode failed\n"); - return 1; - } + // helper function to evaluate a prompt and generate a response + auto generate = [&](const std::string & prompt) { + std::string response; + + const bool is_first = llama_memory_seq_pos_max(llama_get_memory(ctx), 0) == -1; - // --- Sampler --- - llama_sampler *sampler = llama_sampler_init_greedy(); - - // --- Generation loop --- - for (int i = 0; i < 100; i++) { - llama_token tok = llama_sampler_sample(sampler, ctx, 0); - - //std::string piece = llama_vocab_token_to_piece(vocab, tok); - char piece_buf[512]; - int32_t piece_len = llama_token_to_piece( - vocab, - tok, - piece_buf, - sizeof(piece_buf), - 0, // lstrip - true // print special tokens - ); - - if (piece_len > 0) { - printf("%s", piece_buf); + // tokenize the prompt + const int n_prompt_tokens = -llama_tokenize(vocab, prompt.c_str(), prompt.size(), NULL, 0, is_first, true); + std::vector prompt_tokens(n_prompt_tokens); + if (llama_tokenize(vocab, prompt.c_str(), prompt.size(), prompt_tokens.data(), prompt_tokens.size(), is_first, true) < 0) { + GGML_ABORT("failed to tokenize the prompt\n"); } - // feed token back - llama_batch next = llama_batch_init(1, 0, 1); - next.token[0] = tok; - next.pos[0] = 0; - next.seq_id[0]= 0; - next.logits[0]= false; - next.n_tokens = 1; + // prepare a batch for the prompt + llama_batch batch = llama_batch_get_one(prompt_tokens.data(), prompt_tokens.size()); + llama_token new_token_id; + while (true) { + // check if we have enough space in the context to evaluate this batch + int n_ctx = llama_n_ctx(ctx); + int n_ctx_used = llama_memory_seq_pos_max(llama_get_memory(ctx), 0) + 1; + if (n_ctx_used + batch.n_tokens > n_ctx) { + printf("\033[0m\n"); + fprintf(stderr, "context size exceeded\n"); + exit(0); + } + + int ret = llama_decode(ctx, batch); + if (ret != 0) { + GGML_ABORT("failed to decode, ret = %d\n", ret); + } + + // sample the next token + new_token_id = llama_sampler_sample(smpl, ctx, -1); + + // is it an end of generation? + if (llama_vocab_is_eog(vocab, new_token_id)) { + break; + } + + // convert the token to a string, print it and add it to the response + char buf[256]; + int n = llama_token_to_piece(vocab, new_token_id, buf, sizeof(buf), 0, true); + if (n < 0) { + GGML_ABORT("failed to convert token to piece\n"); + } + std::string piece(buf, n); + printf("%s", piece.c_str()); + fflush(stdout); + response += piece; + + // prepare the next batch with the sampled token + batch = llama_batch_get_one(&new_token_id, 1); + } - if (llama_decode(ctx, next) != 0) break; - } + return response; + }; - printf("\n"); + std::vector messages; + std::vector formatted(llama_n_ctx(ctx)); + int prev_len = 0; + while (true) { + // get user input + printf("\033[32m> \033[0m"); + std::string user; + std::getline(std::cin, user); + + if (user.empty()) { + break; + } + + const char * tmpl = llama_model_chat_template(model, /* name */ nullptr); + + // add the user input to the message list and format it + messages.push_back({"user", strdup(user.c_str())}); + int new_len = llama_chat_apply_template(tmpl, messages.data(), messages.size(), true, formatted.data(), formatted.size()); + if (new_len > (int)formatted.size()) { + formatted.resize(new_len); + new_len = llama_chat_apply_template(tmpl, messages.data(), messages.size(), true, formatted.data(), formatted.size()); + } + if (new_len < 0) { + fprintf(stderr, "failed to apply the chat template\n"); + return 1; + } + + // remove previous messages to obtain the prompt to generate the response + std::string prompt(formatted.begin() + prev_len, formatted.begin() + new_len); + + // generate a response + printf("\033[33m"); + std::string response = generate(prompt); + printf("\n\033[0m"); + + // add the response to the messages + messages.push_back({"assistant", strdup(response.c_str())}); + prev_len = llama_chat_apply_template(tmpl, messages.data(), messages.size(), false, nullptr, 0); + if (prev_len < 0) { + fprintf(stderr, "failed to apply the chat template\n"); + return 1; + } + } - llama_sampler_free(sampler); + // free resources + for (auto & msg : messages) { + free(const_cast(msg.content)); + } + llama_sampler_free(smpl); llama_free(ctx); llama_model_free(model); + + return 0; } From 39189b722ee8fb62599329d9973cdcbc69711043 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Tue, 16 Dec 2025 11:55:03 +1030 Subject: [PATCH 067/131] LLM: plugin module - initial commit --- llama/llama-sb.cpp | 47 ++++++----- llama/llama-sb.h | 4 +- llama/main.cpp | 197 +++++++++++++++++---------------------------- 3 files changed, 105 insertions(+), 143 deletions(-) diff --git a/llama/llama-sb.cpp b/llama/llama-sb.cpp index 1e70a52..a07b902 100644 --- a/llama/llama-sb.cpp +++ b/llama/llama-sb.cpp @@ -22,6 +22,30 @@ Llama::Llama() : _n_ctx(0) { } +Llama::~Llama() { + if (_sampler) { + llama_sampler_free(_sampler); + } + if (_ctx) { + llama_free(_ctx); + } + if (_model) { + llama_model_free(_model); + } +} + +void Llama::append_response(const string &response) { + _chat_prompt += response; + _chat_prompt += "\n"; +} + +const string Llama::build_chat_prompt(const string &user_msg) { + _chat_prompt += "User: "; + _chat_prompt += user_msg; + _chat_prompt += "\nAssistant: "; + return _chat_prompt; +} + bool Llama::create(string model_path, int n_ctx, bool disable_log) { if (disable_log) { // only print errors @@ -56,25 +80,6 @@ bool Llama::create(string model_path, int n_ctx, bool disable_log) { return _last_error.empty(); } -Llama::~Llama() { - if (_sampler) { - llama_sampler_free(_sampler); - } - if (_ctx) { - llama_free(_ctx); - } - if (_model) { - llama_model_free(_model); - } -} - -string Llama::build_chat_prompt(const string &user_msg) { - _chat_prompt += "User: "; - _chat_prompt += user_msg; - _chat_prompt += "\nAssistant: "; - return _chat_prompt; -} - void Llama::configure_sampler(float temperature) { if (temperature != _temperature || _sampler == nullptr) { if (_sampler) { @@ -143,3 +148,7 @@ string Llama::generate(const string &prompt, int max_tokens, float temperature, return out; } +void Llama::reset() { + // llama_kv_cache_clear(it->second->ctx); + _chat_prompt.clear(); +} diff --git a/llama/llama-sb.h b/llama/llama-sb.h index 2d4d216..6a4990e 100644 --- a/llama/llama-sb.h +++ b/llama/llama-sb.h @@ -16,6 +16,8 @@ struct Llama { explicit Llama(); ~Llama(); + void append_response(const string &response); + const string build_chat_prompt(const string &user_msg); bool create(string model_path, int n_ctx, bool disable_log); string generate(const string &prompt, int max_tokens = 128, @@ -23,9 +25,9 @@ struct Llama { bool echo = true, bool clear_cache = true); const char *last_error() { return _last_error.c_str(); } + void reset(); private: - string build_chat_prompt(const string &user_msg); void configure_sampler(float temperature); llama_model *_model; diff --git a/llama/main.cpp b/llama/main.cpp index eff3197..00b4e10 100644 --- a/llama/main.cpp +++ b/llama/main.cpp @@ -20,7 +20,7 @@ int g_nextId = 1; robin_hood::unordered_map g_map; -static int get_id(var_s *map, var_s *retval) { +static int get_class_id(var_s *map, var_s *retval) { int result = -1; if (is_map(map)) { int id = map->v.m.id; @@ -34,130 +34,90 @@ static int get_id(var_s *map, var_s *retval) { return result; } -const char *llm_llama_chat(int id, - const char * user_message, - int max_tokens, - float temperature) { - static thread_local string result; - - // auto it = g_llamas.find(h); - // if (it == g_llamas.end()) { - // result = "[invalid llama handle]"; - // return result.c_str(); - // } +static string expand_path(const char *path) { + string result; + if (path && path[0] == '~') { + const char *home = getenv("HOME"); + if (home != nullptr) { + result.append(home); + result.append(path + 1); + } else { + result = path; + } + } else { + result = path; + } + return result; +} - // Llama * llm = it->second.get(); +// +// print llama.chat("Hello") +// +static int cmd_llama_chat(var_s *self, int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + if (argc < 1) { + error(retval, "llama.chat", 1, 3); + } else { + int id = get_class_id(self, retval); + if (id != -1) { + Llama &llama = g_map.at(id); + auto prompt = get_param_str(argc, arg, 0, ""); + int max_tokens = get_param_int(argc, arg, 0, 512); + var_num_t temperature = get_param_num(argc, arg, 0, 0); - // // build accumulated prompt - // string prompt = build_chat_prompt(llm, user_message); + // build accumulated prompt + string updated_prompt = llama.build_chat_prompt(prompt); - // // run generation WITHOUT clearing cache - // result = llm->generate(prompt, - // max_tokens, - // temperature, - // false); // echo = false + // run generation WITHOUT clearing cache + string response = llama.generate(updated_prompt, max_tokens, temperature, false, false); - // // append assistant reply to history - // llm->chat_prompt += result; - // llm->chat_prompt += "\n"; + // append assistant reply to history + llama.append_response(response); - return result.c_str(); + v_setstr(retval, response.c_str()); + result = 1; + } + } + return result; } // -// make the model forget everything +// llama.reset() - make the model forget everything // -void llm_llama_reset() { - // std::lock_guard lock(g_mutex); - - // auto it = g_llamas.find(h); - // if (it == g_llamas.end()) return; - - // llama_kv_cache_clear(it->second->ctx); - // it->second->chat_prompt.clear(); +static int cmd_llama_reset(var_s *self, int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + if (argc != 0) { + error(retval, "llama.reset", 0, 0); + } else { + int id = get_class_id(self, retval); + if (id != -1) { + Llama &llama = g_map.at(id); + llama.reset(); + result = 1; + } + } + return result; } // -// string generate(prompt, max_tokens, temperature) +// print llama.generate("please generate as simple program in BASIC to draw a cat", 1024, 0.8) // -const char *llm_llama_generate(const char * prompt, - int max_tokens, - float temperature) { - // static thread_local string result; - - // std::lock_guard lock(g_mutex); - - // auto it = g_llamas.find(h); - // if (it == g_llamas.end()) { - // result = "[invalid llama handle]"; - // return result.c_str(); - // } - - // try { - // result = it->second->generate(prompt, - // max_tokens, - // temperature); - // } catch (const std::exception & e) { - // result = e.what(); - // } - - // return result.c_str(); - return nullptr; -} - - -static int llm_llama_create(const char *model_path, int n_ctx) { - // std::lock_guard lock(g_mutex); - - // llama_handle id = g_next_id++; - - // try { - // g_llamas[id] = std::make_unique(model_path, n_ctx); - // } catch (...) { - // return 0; - // } - - return 0; -} - -void llm_llama_destroy() { - // std::lock_guard lock(g_mutex); - // g_llamas.erase(h); -} - -// const char *llm_llama_generate(llama_handle h, -// const char *prompt, -// int max_tokens, -// float temperature) { -// static thread_local string result; - -// auto it = g_llamas.find(h); -// if (it == g_llamas.end()) { -// result = "[invalid llama handle]"; -// return result.c_str(); -// } - -// try { -// result = it->second->generate(prompt, max_tokens, temperature); -// } catch (const std::exception &e) { -// result = e.what(); -// } - -// return result.c_str(); -// } - -string expand_path(const char *path) { - string result; - if (path && path[0] == '~') { - const char *home = getenv("HOME"); - if (home != nullptr) { - result.append(home); - result.append(path + 1); - } else { - result = path; - } +static int cmd_llama_generate(var_s *self, int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + if (argc < 1) { + error(retval, "llama.generate", 1, 3); } else { - result = path; + int id = get_class_id(self, retval); + if (id != -1) { + Llama &llama = g_map.at(id); + auto prompt = get_param_str(argc, arg, 0, ""); + int max_tokens = get_param_int(argc, arg, 0, 512); + var_num_t temperature = get_param_num(argc, arg, 0, 0); + + // run generation WITHOUT clearing cache + string response = llama.generate(prompt, max_tokens, temperature, false, true); + v_setstr(retval, response.c_str()); + } } return result; } @@ -171,18 +131,9 @@ static int cmd_create_llama(int argc, slib_par_t *params, var_t *retval) { Llama &llama = g_map[id]; if (llama.create(model, n_ctx, disable_log)) { map_init_id(retval, id, CLASS_ID); - - // v_create_callback(map, "getVoltage", cmd_analoginput_getvoltage); - // v_create_callback(map, "getVoltageSync", cmd_analoginput_getvoltagesync); - // v_create_callback(map, "getReference", cmd_analoginput_getreference); - // v_create_callback(map, "read", cmd_analoginput_read); - // v_create_callback(map, "readSync", cmd_analoginput_readsync); - // v_create_callback(map, "getOverflowCount", cmd_analoginput_getoverflowcount); - // v_create_callback(map, "available", cmd_analoginput_available); - // v_create_callback(map, "readBuffered", cmd_analoginput_readbuffered); - // v_create_callback(map, "getVoltageBuffered", cmd_analoginput_getvoltagebuffered); - // v_create_callback(map, "getSampleRate", cmd_analoginput_getsamplerate); - + v_create_callback(retval, "chat", cmd_llama_chat); + v_create_callback(retval, "generate", cmd_llama_generate); + v_create_callback(retval, "reset", cmd_llama_reset); result = 1; } else { error(retval, llama.last_error()); From bbe0c2fef4f20ebb9dd7232ee6aadf78246f2109 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Wed, 17 Dec 2025 09:40:31 +1030 Subject: [PATCH 068/131] LLM: plugin module - initial commit --- llama/llama-sb.cpp | 82 +++++++++++------- llama/llama-sb.h | 2 +- llama/main.cpp | 3 +- llama/test_main.cpp | 203 ++++++++------------------------------------ 4 files changed, 89 insertions(+), 201 deletions(-) diff --git a/llama/llama-sb.cpp b/llama/llama-sb.cpp index a07b902..37e94b1 100644 --- a/llama/llama-sb.cpp +++ b/llama/llama-sb.cpp @@ -5,11 +5,7 @@ // // Copyright(C) 2026 Chris Warren-Smith -#include -#include -#include #include - #include "llama.h" #include "llama-sb.h" @@ -46,11 +42,11 @@ const string Llama::build_chat_prompt(const string &user_msg) { return _chat_prompt; } -bool Llama::create(string model_path, int n_ctx, bool disable_log) { +bool Llama::construct(string model_path, int n_ctx, bool disable_log) { if (disable_log) { // only print errors llama_log_set([](enum ggml_log_level level, const char * text, void * /* user_data */) { - if (level >= GGML_LOG_LEVEL_ERROR) { + if (level >= GGML_LOG_LEVEL_ERROR && text[0] != '.' && text[0] != '\n') { fprintf(stderr, "%s", text); } }, nullptr); @@ -59,7 +55,7 @@ bool Llama::create(string model_path, int n_ctx, bool disable_log) { ggml_backend_load_all(); llama_model_params mparams = llama_model_default_params(); - mparams.n_gpu_layers = 0; + mparams.n_gpu_layers = 99; _model = llama_model_load_from_file(model_path.c_str(), mparams); if (!_model) { @@ -68,13 +64,13 @@ bool Llama::create(string model_path, int n_ctx, bool disable_log) { llama_context_params cparams = llama_context_default_params(); cparams.n_ctx = n_ctx; cparams.n_batch = n_ctx; + cparams.no_perf = true; _ctx = llama_init_from_model(_model, cparams); if (!_ctx) { _last_error = "failed to create context"; } else { _vocab = llama_model_get_vocab(_model); - configure_sampler(0); } } return _last_error.empty(); @@ -82,10 +78,11 @@ bool Llama::create(string model_path, int n_ctx, bool disable_log) { void Llama::configure_sampler(float temperature) { if (temperature != _temperature || _sampler == nullptr) { - if (_sampler) { + if (_sampler != nullptr) { llama_sampler_free(_sampler); } auto sparams = llama_sampler_chain_default_params(); + sparams.no_perf = false; _sampler = llama_sampler_chain_init(sparams); _temperature = temperature; @@ -93,18 +90,13 @@ void Llama::configure_sampler(float temperature) { if (temperature <= 0.0f) { llama_sampler_chain_add(_sampler, llama_sampler_init_greedy()); } else { + llama_sampler_chain_add(_sampler, llama_sampler_init_min_p(0.05f, 1)); llama_sampler_chain_add(_sampler, llama_sampler_init_temp(temperature)); + llama_sampler_chain_add(_sampler, llama_sampler_init_dist(LLAMA_DEFAULT_SEED)); } } } -static std::vector tokenize(const llama_vocab *vocab, const string &text) { - int n = -llama_tokenize(vocab, text.c_str(), text.size(), nullptr, 0, true, true); - std::vector tokens(n); - llama_tokenize(vocab, text.c_str(), text.size(), tokens.data(), tokens.size(), true, true); - return tokens; -} - string Llama::generate(const string &prompt, int max_tokens, float temperature, bool echo, bool clear_cache) { string out; @@ -112,37 +104,67 @@ string Llama::generate(const string &prompt, int max_tokens, float temperature, // llama_kv_cache_clear(_ctx); } - auto prompt_tokens = tokenize(_vocab, prompt); + // find the number of tokens in the prompt + int n_prompt = -llama_tokenize(_vocab, prompt.c_str(), prompt.size(), nullptr, 0, true, true); + + // allocate space for the tokens and tokenize the prompt + std::vector prompt_tokens(n_prompt); + if (llama_tokenize(_vocab, prompt.c_str(), prompt.size(), prompt_tokens.data(), prompt_tokens.size(), true, true) < 0) { + _last_error = "failed tokenize the prompt"; + return out; + } + + // initialize the sampler configure_sampler(temperature); + // prepare a batch for the prompt llama_batch batch = llama_batch_get_one(prompt_tokens.data(), prompt_tokens.size()); + if (llama_model_has_encoder(_model)) { + if (llama_encode(_ctx, batch)) { + _last_error = "failed to eval"; + return out; + } - if (llama_decode(_ctx, batch)) { - _last_error = "decode failed"; - return out; + llama_token decoder_start_token_id = llama_model_decoder_start_token(_model); + if (decoder_start_token_id == LLAMA_TOKEN_NULL) { + decoder_start_token_id = llama_vocab_bos(_vocab); + } + + batch = llama_batch_get_one(&decoder_start_token_id, 1); } if (echo) { out += prompt; } - for (int i = 0; i < max_tokens; ++i) { - llama_token tok = llama_sampler_sample(_sampler, _ctx, -1); - - if (llama_vocab_is_eog(_vocab, tok)) { + for (int n_pos = 0; n_pos + batch.n_tokens < n_prompt + max_tokens;) { + // evaluate the current batch with the transformer model + if (llama_decode(_ctx, batch)) { + _last_error = "failed to eval"; break; } - char buf[128]; - int n = llama_token_to_piece(_vocab, tok, buf, sizeof(buf), 0, true); + n_pos += batch.n_tokens; - if (n > 0) { - out.append(buf, n); + // sample the next token + llama_token new_token_id = llama_sampler_sample(_sampler, _ctx, -1); + + // is it an end of generation? + if (llama_vocab_is_eog(_vocab, new_token_id)) { + break; } - batch = llama_batch_get_one(&tok, 1); - if (llama_decode(_ctx, batch)) { + + char buf[128]; + int n = llama_token_to_piece(_vocab, new_token_id, buf, sizeof(buf), 0, true); + if (n < 0) { + _last_error = "failed to convert token to piece"; break; + } else if (n > 0) { + out.append(buf, n); } + + // prepare the next batch with the sampled token + batch = llama_batch_get_one(&new_token_id, 1); } return out; diff --git a/llama/llama-sb.h b/llama/llama-sb.h index 6a4990e..830f0ed 100644 --- a/llama/llama-sb.h +++ b/llama/llama-sb.h @@ -18,7 +18,7 @@ struct Llama { void append_response(const string &response); const string build_chat_prompt(const string &user_msg); - bool create(string model_path, int n_ctx, bool disable_log); + bool construct(string model_path, int n_ctx, bool disable_log); string generate(const string &prompt, int max_tokens = 128, float temperature = 0.8f, diff --git a/llama/main.cpp b/llama/main.cpp index 00b4e10..c05e196 100644 --- a/llama/main.cpp +++ b/llama/main.cpp @@ -117,6 +117,7 @@ static int cmd_llama_generate(var_s *self, int argc, slib_par_t *arg, var_s *ret // run generation WITHOUT clearing cache string response = llama.generate(prompt, max_tokens, temperature, false, true); v_setstr(retval, response.c_str()); + result = 1; } } return result; @@ -129,7 +130,7 @@ static int cmd_create_llama(int argc, slib_par_t *params, var_t *retval) { int disable_log = get_param_int(argc, params, 0, 1); int id = ++g_nextId; Llama &llama = g_map[id]; - if (llama.create(model, n_ctx, disable_log)) { + if (llama.construct(model, n_ctx, disable_log)) { map_init_id(retval, id, CLASS_ID); v_create_callback(retval, "chat", cmd_llama_chat); v_create_callback(retval, "generate", cmd_llama_generate); diff --git a/llama/test_main.cpp b/llama/test_main.cpp index b25cde5..3c813c1 100644 --- a/llama/test_main.cpp +++ b/llama/test_main.cpp @@ -1,24 +1,26 @@ -#include "llama.h" +#include "llama-sb.h" + #include #include -#include -#include -#include static void print_usage(int, char ** argv) { printf("\nexample usage:\n"); - printf("\n %s -m model.gguf [-c context_size] [-ngl n_gpu_layers]\n", argv[0]); + printf("\n %s -m model.gguf [-n n_predict] [-ngl n_gpu_layers] [prompt]\n", argv[0]); printf("\n"); } int main(int argc, char ** argv) { + // path to the model gguf file std::string model_path; - int ngl = 99; - int n_ctx = 2048; + // prompt to generate text from + std::string prompt = "Happy friday"; + // number of tokens to predict + int n_predict = 32; // parse command line arguments - for (int i = 1; i < argc; i++) { - try { + { + int i = 1; + for (; i < argc; i++) { if (strcmp(argv[i], "-m") == 0) { if (i + 1 < argc) { model_path = argv[++i]; @@ -26,182 +28,45 @@ int main(int argc, char ** argv) { print_usage(argc, argv); return 1; } - } else if (strcmp(argv[i], "-c") == 0) { + } else if (strcmp(argv[i], "-n") == 0) { if (i + 1 < argc) { - n_ctx = std::stoi(argv[++i]); - } else { - print_usage(argc, argv); - return 1; - } - } else if (strcmp(argv[i], "-ngl") == 0) { - if (i + 1 < argc) { - ngl = std::stoi(argv[++i]); + try { + n_predict = std::stoi(argv[++i]); + } catch (...) { + print_usage(argc, argv); + return 1; + } } else { print_usage(argc, argv); return 1; } } else { - print_usage(argc, argv); - return 1; + // prompt starts here + break; } - } catch (std::exception & e) { - fprintf(stderr, "error: %s\n", e.what()); + } + if (model_path.empty()) { print_usage(argc, argv); return 1; } - } - if (model_path.empty()) { - print_usage(argc, argv); - return 1; - } - - // only print errors - llama_log_set([](enum ggml_log_level level, const char * text, void * /* user_data */) { - if (level >= GGML_LOG_LEVEL_ERROR) { - fprintf(stderr, "%s", text); - } - }, nullptr); - - // load dynamic backends - ggml_backend_load_all(); - - // initialize the model - llama_model_params model_params = llama_model_default_params(); - model_params.n_gpu_layers = ngl; - - llama_model * model = llama_model_load_from_file(model_path.c_str(), model_params); - if (!model) { - fprintf(stderr , "%s: error: unable to load model\n" , __func__); - return 1; - } - - const llama_vocab * vocab = llama_model_get_vocab(model); - - // initialize the context - llama_context_params ctx_params = llama_context_default_params(); - ctx_params.n_ctx = n_ctx; - ctx_params.n_batch = n_ctx; - - llama_context * ctx = llama_init_from_model(model, ctx_params); - if (!ctx) { - fprintf(stderr , "%s: error: failed to create the llama_context\n" , __func__); - return 1; - } - - // initialize the sampler - llama_sampler * smpl = llama_sampler_chain_init(llama_sampler_chain_default_params()); - llama_sampler_chain_add(smpl, llama_sampler_init_min_p(0.05f, 1)); - llama_sampler_chain_add(smpl, llama_sampler_init_temp(0.8f)); - llama_sampler_chain_add(smpl, llama_sampler_init_dist(LLAMA_DEFAULT_SEED)); - - // helper function to evaluate a prompt and generate a response - auto generate = [&](const std::string & prompt) { - std::string response; - - const bool is_first = llama_memory_seq_pos_max(llama_get_memory(ctx), 0) == -1; - - // tokenize the prompt - const int n_prompt_tokens = -llama_tokenize(vocab, prompt.c_str(), prompt.size(), NULL, 0, is_first, true); - std::vector prompt_tokens(n_prompt_tokens); - if (llama_tokenize(vocab, prompt.c_str(), prompt.size(), prompt_tokens.data(), prompt_tokens.size(), is_first, true) < 0) { - GGML_ABORT("failed to tokenize the prompt\n"); - } - - // prepare a batch for the prompt - llama_batch batch = llama_batch_get_one(prompt_tokens.data(), prompt_tokens.size()); - llama_token new_token_id; - while (true) { - // check if we have enough space in the context to evaluate this batch - int n_ctx = llama_n_ctx(ctx); - int n_ctx_used = llama_memory_seq_pos_max(llama_get_memory(ctx), 0) + 1; - if (n_ctx_used + batch.n_tokens > n_ctx) { - printf("\033[0m\n"); - fprintf(stderr, "context size exceeded\n"); - exit(0); + if (i < argc) { + prompt = argv[i++]; + for (; i < argc; i++) { + prompt += " "; + prompt += argv[i]; } - - int ret = llama_decode(ctx, batch); - if (ret != 0) { - GGML_ABORT("failed to decode, ret = %d\n", ret); - } - - // sample the next token - new_token_id = llama_sampler_sample(smpl, ctx, -1); - - // is it an end of generation? - if (llama_vocab_is_eog(vocab, new_token_id)) { - break; - } - - // convert the token to a string, print it and add it to the response - char buf[256]; - int n = llama_token_to_piece(vocab, new_token_id, buf, sizeof(buf), 0, true); - if (n < 0) { - GGML_ABORT("failed to convert token to piece\n"); - } - std::string piece(buf, n); - printf("%s", piece.c_str()); - fflush(stdout); - response += piece; - - // prepare the next batch with the sampled token - batch = llama_batch_get_one(&new_token_id, 1); - } - - return response; - }; - - std::vector messages; - std::vector formatted(llama_n_ctx(ctx)); - int prev_len = 0; - while (true) { - // get user input - printf("\033[32m> \033[0m"); - std::string user; - std::getline(std::cin, user); - - if (user.empty()) { - break; } + } - const char * tmpl = llama_model_chat_template(model, /* name */ nullptr); - - // add the user input to the message list and format it - messages.push_back({"user", strdup(user.c_str())}); - int new_len = llama_chat_apply_template(tmpl, messages.data(), messages.size(), true, formatted.data(), formatted.size()); - if (new_len > (int)formatted.size()) { - formatted.resize(new_len); - new_len = llama_chat_apply_template(tmpl, messages.data(), messages.size(), true, formatted.data(), formatted.size()); - } - if (new_len < 0) { - fprintf(stderr, "failed to apply the chat template\n"); - return 1; - } - - // remove previous messages to obtain the prompt to generate the response - std::string prompt(formatted.begin() + prev_len, formatted.begin() + new_len); - - // generate a response + Llama llama; + if (llama.construct(model_path, 1024, true)) { + string out = llama. generate(prompt, n_predict, 0.8f, true, true); printf("\033[33m"); - std::string response = generate(prompt); + printf(out.c_str()); printf("\n\033[0m"); - - // add the response to the messages - messages.push_back({"assistant", strdup(response.c_str())}); - prev_len = llama_chat_apply_template(tmpl, messages.data(), messages.size(), false, nullptr, 0); - if (prev_len < 0) { - fprintf(stderr, "failed to apply the chat template\n"); - return 1; - } - } - - // free resources - for (auto & msg : messages) { - free(const_cast(msg.content)); + } else { + fprintf(stderr, "ERR: %s\n", llama.last_error()); } - llama_sampler_free(smpl); - llama_free(ctx); - llama_model_free(model); return 0; } From 9e2c60eb5faa3a108c3aea999671110702fb6a88 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Wed, 17 Dec 2025 11:24:33 +1030 Subject: [PATCH 069/131] LLM: plugin module - initial commit --- llama/llama-sb.cpp | 10 +------ llama/llama-sb.h | 6 +---- llama/main.cpp | 16 +++++------- llama/test_main.cpp | 63 +++++++++++++++++++++------------------------ 4 files changed, 39 insertions(+), 56 deletions(-) diff --git a/llama/llama-sb.cpp b/llama/llama-sb.cpp index 37e94b1..34772c6 100644 --- a/llama/llama-sb.cpp +++ b/llama/llama-sb.cpp @@ -97,13 +97,9 @@ void Llama::configure_sampler(float temperature) { } } -string Llama::generate(const string &prompt, int max_tokens, float temperature, bool echo, bool clear_cache) { +string Llama::generate(const string &prompt, int max_tokens, float temperature) { string out; - if (clear_cache) { - // llama_kv_cache_clear(_ctx); - } - // find the number of tokens in the prompt int n_prompt = -llama_tokenize(_vocab, prompt.c_str(), prompt.size(), nullptr, 0, true, true); @@ -133,10 +129,6 @@ string Llama::generate(const string &prompt, int max_tokens, float temperature, batch = llama_batch_get_one(&decoder_start_token_id, 1); } - if (echo) { - out += prompt; - } - for (int n_pos = 0; n_pos + batch.n_tokens < n_prompt + max_tokens;) { // evaluate the current batch with the transformer model if (llama_decode(_ctx, batch)) { diff --git a/llama/llama-sb.h b/llama/llama-sb.h index 830f0ed..71f6bbf 100644 --- a/llama/llama-sb.h +++ b/llama/llama-sb.h @@ -19,11 +19,7 @@ struct Llama { void append_response(const string &response); const string build_chat_prompt(const string &user_msg); bool construct(string model_path, int n_ctx, bool disable_log); - string generate(const string &prompt, - int max_tokens = 128, - float temperature = 0.8f, - bool echo = true, - bool clear_cache = true); + string generate(const string &prompt, int max_tokens, float temperature); const char *last_error() { return _last_error.c_str(); } void reset(); diff --git a/llama/main.cpp b/llama/main.cpp index c05e196..edf16f7 100644 --- a/llama/main.cpp +++ b/llama/main.cpp @@ -62,14 +62,14 @@ static int cmd_llama_chat(var_s *self, int argc, slib_par_t *arg, var_s *retval) if (id != -1) { Llama &llama = g_map.at(id); auto prompt = get_param_str(argc, arg, 0, ""); - int max_tokens = get_param_int(argc, arg, 0, 512); - var_num_t temperature = get_param_num(argc, arg, 0, 0); + int max_tokens = get_param_int(argc, arg, 1, 32); + var_num_t temperature = get_param_num(argc, arg, 2, 0.8f); // build accumulated prompt string updated_prompt = llama.build_chat_prompt(prompt); // run generation WITHOUT clearing cache - string response = llama.generate(updated_prompt, max_tokens, temperature, false, false); + string response = llama.generate(updated_prompt, max_tokens, temperature); // append assistant reply to history llama.append_response(response); @@ -111,11 +111,9 @@ static int cmd_llama_generate(var_s *self, int argc, slib_par_t *arg, var_s *ret if (id != -1) { Llama &llama = g_map.at(id); auto prompt = get_param_str(argc, arg, 0, ""); - int max_tokens = get_param_int(argc, arg, 0, 512); - var_num_t temperature = get_param_num(argc, arg, 0, 0); - - // run generation WITHOUT clearing cache - string response = llama.generate(prompt, max_tokens, temperature, false, true); + int max_tokens = get_param_int(argc, arg, 1, 32); + var_num_t temperature = get_param_num(argc, arg, 2, 0.8f); + string response = llama.generate(prompt, max_tokens, temperature); v_setstr(retval, response.c_str()); result = 1; } @@ -127,7 +125,7 @@ static int cmd_create_llama(int argc, slib_par_t *params, var_t *retval) { int result; auto model = expand_path(get_param_str(argc, params, 0, "")); int n_ctx = get_param_int(argc, params, 0, 2048); - int disable_log = get_param_int(argc, params, 0, 1); + int disable_log = get_param_int(argc, params, 1, 1); int id = ++g_nextId; Llama &llama = g_map[id]; if (llama.construct(model, n_ctx, disable_log)) { diff --git a/llama/test_main.cpp b/llama/test_main.cpp index 3c813c1..430a6b3 100644 --- a/llama/test_main.cpp +++ b/llama/test_main.cpp @@ -1,5 +1,4 @@ #include "llama-sb.h" - #include #include @@ -18,49 +17,47 @@ int main(int argc, char ** argv) { int n_predict = 32; // parse command line arguments - { - int i = 1; - for (; i < argc; i++) { - if (strcmp(argv[i], "-m") == 0) { - if (i + 1 < argc) { - model_path = argv[++i]; - } else { - print_usage(argc, argv); - return 1; - } - } else if (strcmp(argv[i], "-n") == 0) { - if (i + 1 < argc) { - try { - n_predict = std::stoi(argv[++i]); - } catch (...) { - print_usage(argc, argv); - return 1; - } - } else { + int i = 1; + for (; i < argc; i++) { + if (strcmp(argv[i], "-m") == 0) { + if (i + 1 < argc) { + model_path = argv[++i]; + } else { + print_usage(argc, argv); + return 1; + } + } else if (strcmp(argv[i], "-n") == 0) { + if (i + 1 < argc) { + try { + n_predict = std::stoi(argv[++i]); + } catch (...) { print_usage(argc, argv); return 1; } } else { - // prompt starts here - break; + print_usage(argc, argv); + return 1; } + } else { + // prompt starts here + break; } - if (model_path.empty()) { - print_usage(argc, argv); - return 1; - } - if (i < argc) { - prompt = argv[i++]; - for (; i < argc; i++) { - prompt += " "; - prompt += argv[i]; - } + } + if (model_path.empty()) { + print_usage(argc, argv); + return 1; + } + if (i < argc) { + prompt = argv[i++]; + for (; i < argc; i++) { + prompt += " "; + prompt += argv[i]; } } Llama llama; if (llama.construct(model_path, 1024, true)) { - string out = llama. generate(prompt, n_predict, 0.8f, true, true); + string out = llama. generate(prompt, n_predict, 0.8f); printf("\033[33m"); printf(out.c_str()); printf("\n\033[0m"); From 3ae5e0487a233f6333f58a9e6b99227583820779 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Mon, 22 Dec 2025 11:53:56 +1030 Subject: [PATCH 070/131] LLM: plugin module - initial commit --- llama/CMakeLists.txt | 4 ++ llama/llama-sb.cpp | 68 +++++++++++++------------ llama/llama-sb.h | 29 +++++++++-- llama/main.cpp | 118 ++++++++++++++++++++++++++++++++++++++----- llama/test_main.cpp | 4 +- 5 files changed, 170 insertions(+), 53 deletions(-) diff --git a/llama/CMakeLists.txt b/llama/CMakeLists.txt index 23f41b9..aad1e62 100644 --- a/llama/CMakeLists.txt +++ b/llama/CMakeLists.txt @@ -33,6 +33,7 @@ set(GGML_BUILD_TESTS OFF CACHE BOOL "" FORCE) set(GGML_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) # CPU-only flags +set(GGML_OPENMP OFF CACHE BOOL "" FORCE) set(GGML_CUDA OFF CACHE BOOL "" FORCE) set(GGML_METAL OFF CACHE BOOL "" FORCE) set(GGML_OPENCL OFF CACHE BOOL "" FORCE) @@ -114,6 +115,9 @@ set_target_properties(llm_test PROPERTIES # Android native library # ------------------------------------------------------------------ if (ANDROID) + set(GGML_LLAMAFILE OFF CACHE BOOL "" FORCE) + set(GGML_BLAS OFF CACHE BOOL "" FORCE) + # CMake sets ANDROID when using the Android toolchain # Re‑use the same source files for the Android .so add_library(llm_android SHARED diff --git a/llama/llama-sb.cpp b/llama/llama-sb.cpp index 34772c6..7922065 100644 --- a/llama/llama-sb.cpp +++ b/llama/llama-sb.cpp @@ -15,7 +15,17 @@ Llama::Llama() : _sampler(nullptr), _vocab(nullptr), _temperature(0), - _n_ctx(0) { + _top_k(0), + _top_p(1.0f), + _min_p(0.0f), + _max_tokens(150), + _log_level(GGML_LOG_LEVEL_NONE) { + llama_log_set([](enum ggml_log_level level, const char * text, void *user_data) { + Llama *llama = (Llama *)user_data; + if (level > llama->_log_level) { + fprintf(stderr, "LLAMA: %s", text); + } + }, this); } Llama::~Llama() { @@ -42,20 +52,11 @@ const string Llama::build_chat_prompt(const string &user_msg) { return _chat_prompt; } -bool Llama::construct(string model_path, int n_ctx, bool disable_log) { - if (disable_log) { - // only print errors - llama_log_set([](enum ggml_log_level level, const char * text, void * /* user_data */) { - if (level >= GGML_LOG_LEVEL_ERROR && text[0] != '.' && text[0] != '\n') { - fprintf(stderr, "%s", text); - } - }, nullptr); - } - +bool Llama::construct(string model_path, int n_ctx, int n_batch) { ggml_backend_load_all(); llama_model_params mparams = llama_model_default_params(); - mparams.n_gpu_layers = 99; + mparams.n_gpu_layers = 0; _model = llama_model_load_from_file(model_path.c_str(), mparams); if (!_model) { @@ -63,41 +64,42 @@ bool Llama::construct(string model_path, int n_ctx, bool disable_log) { } else { llama_context_params cparams = llama_context_default_params(); cparams.n_ctx = n_ctx; - cparams.n_batch = n_ctx; + cparams.n_batch = n_batch; cparams.no_perf = true; - _ctx = llama_init_from_model(_model, cparams); if (!_ctx) { _last_error = "failed to create context"; } else { _vocab = llama_model_get_vocab(_model); + + auto sparams = llama_sampler_chain_default_params(); + sparams.no_perf = false; + _sampler = llama_sampler_chain_init(sparams); } } return _last_error.empty(); } -void Llama::configure_sampler(float temperature) { - if (temperature != _temperature || _sampler == nullptr) { - if (_sampler != nullptr) { - llama_sampler_free(_sampler); +void Llama::configure_sampler() { + llama_sampler_reset(_sampler); + if (_temperature <= 0.0f) { + llama_sampler_chain_add(_sampler, llama_sampler_init_greedy()); + } else { + llama_sampler_chain_add(_sampler, llama_sampler_init_temp(_temperature)); + if (_top_k > 0) { + llama_sampler_chain_add(_sampler, llama_sampler_init_top_k(_top_k)); } - auto sparams = llama_sampler_chain_default_params(); - sparams.no_perf = false; - _sampler = llama_sampler_chain_init(sparams); - _temperature = temperature; - - // llama_sampler_chain_reset(sampler); - if (temperature <= 0.0f) { - llama_sampler_chain_add(_sampler, llama_sampler_init_greedy()); - } else { - llama_sampler_chain_add(_sampler, llama_sampler_init_min_p(0.05f, 1)); - llama_sampler_chain_add(_sampler, llama_sampler_init_temp(temperature)); - llama_sampler_chain_add(_sampler, llama_sampler_init_dist(LLAMA_DEFAULT_SEED)); + if (_top_p < 1.0f) { + llama_sampler_chain_add(_sampler, llama_sampler_init_top_p(_top_p, 1)); + } + if (_min_p > 0.0f) { + llama_sampler_chain_add(_sampler, llama_sampler_init_min_p(_min_p, 1)); } + llama_sampler_chain_add(_sampler, llama_sampler_init_dist(LLAMA_DEFAULT_SEED)); } } -string Llama::generate(const string &prompt, int max_tokens, float temperature) { +string Llama::generate(const string &prompt) { string out; // find the number of tokens in the prompt @@ -111,7 +113,7 @@ string Llama::generate(const string &prompt, int max_tokens, float temperature) } // initialize the sampler - configure_sampler(temperature); + configure_sampler(); // prepare a batch for the prompt llama_batch batch = llama_batch_get_one(prompt_tokens.data(), prompt_tokens.size()); @@ -129,7 +131,7 @@ string Llama::generate(const string &prompt, int max_tokens, float temperature) batch = llama_batch_get_one(&decoder_start_token_id, 1); } - for (int n_pos = 0; n_pos + batch.n_tokens < n_prompt + max_tokens;) { + for (int n_pos = 0; n_pos + batch.n_tokens < n_prompt + _max_tokens;) { // evaluate the current batch with the transformer model if (llama_decode(_ctx, batch)) { _last_error = "failed to eval"; diff --git a/llama/llama-sb.h b/llama/llama-sb.h index 71f6bbf..ccdf881 100644 --- a/llama/llama-sb.h +++ b/llama/llama-sb.h @@ -16,15 +16,32 @@ struct Llama { explicit Llama(); ~Llama(); + // init + bool construct(string model_path, int n_ctx, int n_batch); + + // generation + string generate(const string &prompt); + + // generation parameters + void set_max_tokens(int max_tokens) { _max_tokens = max_tokens; } + void set_min_p(float min_p) { _min_p = min_p; } + void set_temperature(float temperature) { _temperature = temperature; } + void set_top_k(int top_k) { _top_k = top_k; } + void set_top_p(float top_p) { _top_p = top_p; } + + // messages void append_response(const string &response); + void append_user_message(const string &user_msg); + const string& get_chat_history() const; const string build_chat_prompt(const string &user_msg); - bool construct(string model_path, int n_ctx, bool disable_log); - string generate(const string &prompt, int max_tokens, float temperature); + + // error handling const char *last_error() { return _last_error.c_str(); } + void set_log_level(int level) { _log_level = level; } void reset(); private: - void configure_sampler(float temperature); + void configure_sampler(); llama_model *_model; llama_context *_ctx; @@ -33,5 +50,9 @@ struct Llama { string _chat_prompt; string _last_error; float _temperature; - int _n_ctx; + float _top_p; + float _min_p; + int _top_k; + int _max_tokens; + int _log_level; }; diff --git a/llama/main.cpp b/llama/main.cpp index edf16f7..19f3ca4 100644 --- a/llama/main.cpp +++ b/llama/main.cpp @@ -50,26 +50,111 @@ static string expand_path(const char *path) { return result; } +// +// llama.set_max_tokens(50) +// +static int cmd_llama_set_max_tokens(var_s *self, int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + if (argc != 1) { + error(retval, "llama.set_max_tokens", 1, 1); + } else { + int id = get_class_id(self, retval); + if (id != -1) { + Llama &llama = g_map.at(id); + llama.set_max_tokens(get_param_int(argc, arg, 0, 0)); + result = 1; + } + } + return result; +} + +// +// llama.set_min_p(0.5) +// +static int cmd_llama_set_min_p(var_s *self, int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + if (argc != 1) { + error(retval, "llama.set_min_p", 1, 1); + } else { + int id = get_class_id(self, retval); + if (id != -1) { + Llama &llama = g_map.at(id); + llama.set_min_p(get_param_num(argc, arg, 0, 0)); + result = 1; + } + } + return result; +} + +// +// llama.set_temperature(0.8) +// +static int cmd_llama_set_temperature(var_s *self, int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + if (argc != 1) { + error(retval, "llama.set_temperature", 1, 1); + } else { + int id = get_class_id(self, retval); + if (id != -1) { + Llama &llama = g_map.at(id); + llama.set_temperature(get_param_num(argc, arg, 0, 0)); + result = 1; + } + } + return result; +} + +// +// llama.set_set_top_k(10.0) +// +static int cmd_llama_set_top_k(var_s *self, int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + if (argc != 1) { + error(retval, "llama.set_top_k", 1, 1); + } else { + int id = get_class_id(self, retval); + if (id != -1) { + Llama &llama = g_map.at(id); + llama.set_top_k(get_param_int(argc, arg, 0, 0)); + result = 1; + } + } + return result; +} + +static int cmd_llama_set_top_p(var_s *self, int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + if (argc != 1) { + error(retval, "llama.set_top_p", 1, 1); + } else { + int id = get_class_id(self, retval); + if (id != -1) { + Llama &llama = g_map.at(id); + llama.set_top_p(get_param_num(argc, arg, 0, 0)); + result = 1; + } + } + return result; +} + // // print llama.chat("Hello") // static int cmd_llama_chat(var_s *self, int argc, slib_par_t *arg, var_s *retval) { int result = 0; - if (argc < 1) { - error(retval, "llama.chat", 1, 3); + if (argc != 1) { + error(retval, "llama.chat", 1, 1); } else { int id = get_class_id(self, retval); if (id != -1) { Llama &llama = g_map.at(id); auto prompt = get_param_str(argc, arg, 0, ""); - int max_tokens = get_param_int(argc, arg, 1, 32); - var_num_t temperature = get_param_num(argc, arg, 2, 0.8f); // build accumulated prompt string updated_prompt = llama.build_chat_prompt(prompt); // run generation WITHOUT clearing cache - string response = llama.generate(updated_prompt, max_tokens, temperature); + string response = llama.generate(updated_prompt); // append assistant reply to history llama.append_response(response); @@ -100,20 +185,18 @@ static int cmd_llama_reset(var_s *self, int argc, slib_par_t *arg, var_s *retval } // -// print llama.generate("please generate as simple program in BASIC to draw a cat", 1024, 0.8) +// print llama.generate("please generate as simple program in BASIC to draw a cat") // static int cmd_llama_generate(var_s *self, int argc, slib_par_t *arg, var_s *retval) { int result = 0; - if (argc < 1) { - error(retval, "llama.generate", 1, 3); + if (argc != 1) { + error(retval, "llama.generate", 1, 1); } else { int id = get_class_id(self, retval); if (id != -1) { Llama &llama = g_map.at(id); auto prompt = get_param_str(argc, arg, 0, ""); - int max_tokens = get_param_int(argc, arg, 1, 32); - var_num_t temperature = get_param_num(argc, arg, 2, 0.8f); - string response = llama.generate(prompt, max_tokens, temperature); + string response = llama.generate(prompt); v_setstr(retval, response.c_str()); result = 1; } @@ -124,12 +207,19 @@ static int cmd_llama_generate(var_s *self, int argc, slib_par_t *arg, var_s *ret static int cmd_create_llama(int argc, slib_par_t *params, var_t *retval) { int result; auto model = expand_path(get_param_str(argc, params, 0, "")); - int n_ctx = get_param_int(argc, params, 0, 2048); - int disable_log = get_param_int(argc, params, 1, 1); + auto n_ctx = get_param_int(argc, params, 0, 2048); + auto n_batch = get_param_int(argc, params, 1, 1024); + auto temperature = get_param_num(argc, params, 2, 0.25); int id = ++g_nextId; Llama &llama = g_map[id]; - if (llama.construct(model, n_ctx, disable_log)) { + if (llama.construct(model, n_ctx, n_batch)) { + llama.set_temperature(temperature); map_init_id(retval, id, CLASS_ID); + v_create_callback(retval, "set_max_tokens", cmd_llama_set_max_tokens); + v_create_callback(retval, "set_min_p", cmd_llama_set_min_p); + v_create_callback(retval, "set_temperature", cmd_llama_set_temperature); + v_create_callback(retval, "set_top_k", cmd_llama_set_top_k); + v_create_callback(retval, "set_top_p", cmd_llama_set_top_p); v_create_callback(retval, "chat", cmd_llama_chat); v_create_callback(retval, "generate", cmd_llama_generate); v_create_callback(retval, "reset", cmd_llama_reset); diff --git a/llama/test_main.cpp b/llama/test_main.cpp index 430a6b3..8900f94 100644 --- a/llama/test_main.cpp +++ b/llama/test_main.cpp @@ -56,8 +56,8 @@ int main(int argc, char ** argv) { } Llama llama; - if (llama.construct(model_path, 1024, true)) { - string out = llama. generate(prompt, n_predict, 0.8f); + if (llama.construct(model_path, 1024, 1024)) { + string out = llama.generate(prompt); printf("\033[33m"); printf(out.c_str()); printf("\n\033[0m"); From 47533165a11fb28a66c79bd8581c822b559ade93 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Mon, 22 Dec 2025 16:35:07 +1030 Subject: [PATCH 071/131] added sample --- llama/samples/chat.bas | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 llama/samples/chat.bas diff --git a/llama/samples/chat.bas b/llama/samples/chat.bas new file mode 100644 index 0000000..99d5138 --- /dev/null +++ b/llama/samples/chat.bas @@ -0,0 +1,12 @@ + +const llama = llm.llama("qwen.gguf", 1024) + +print llama.generate("Write a BASIC program", 256, 0.2) + +print llama.chat("Hello") +print llama.chat("Write a BASIC program to draw a cat") +print llama.chat("Now add color") + +llama.reset() + +print llama.chat("Who are you?") From fc00a831be8a59c40d19fbde46a20c31ae175ce4 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Wed, 24 Dec 2025 15:34:28 +1030 Subject: [PATCH 072/131] LLM: plugin module - added repeat penalty handling --- llama/llama-sb.cpp | 117 +++++++++++++++++++++++++++-------------- llama/llama-sb.h | 5 ++ llama/main.cpp | 51 ++++++++++++++++-- llama/samples/chat.bas | 89 ++++++++++++++++++++++++++++--- 4 files changed, 212 insertions(+), 50 deletions(-) diff --git a/llama/llama-sb.cpp b/llama/llama-sb.cpp index 7922065..831dc7f 100644 --- a/llama/llama-sb.cpp +++ b/llama/llama-sb.cpp @@ -5,6 +5,7 @@ // // Copyright(C) 2026 Chris Warren-Smith +#include #include #include "llama.h" #include "llama-sb.h" @@ -14,12 +15,14 @@ Llama::Llama() : _ctx(nullptr), _sampler(nullptr), _vocab(nullptr), + _penalty_last_n(64), + _penalty_repeat(1.1f), _temperature(0), _top_k(0), _top_p(1.0f), _min_p(0.0f), _max_tokens(150), - _log_level(GGML_LOG_LEVEL_NONE) { + _log_level(GGML_LOG_LEVEL_CONT) { llama_log_set([](enum ggml_log_level level, const char * text, void *user_data) { Llama *llama = (Llama *)user_data; if (level > llama->_log_level) { @@ -82,6 +85,10 @@ bool Llama::construct(string model_path, int n_ctx, int n_batch) { void Llama::configure_sampler() { llama_sampler_reset(_sampler); + if (_penalty_last_n != 0 && _penalty_repeat != 1.0f) { + auto penalties = llama_sampler_init_penalties(_penalty_last_n, _penalty_repeat, 0.0f, 0.0f); + llama_sampler_chain_add(_sampler, penalties); + } if (_temperature <= 0.0f) { llama_sampler_chain_add(_sampler, llama_sampler_init_greedy()); } else { @@ -99,72 +106,104 @@ void Llama::configure_sampler() { } } +void Llama::reset() { + // llama_kv_cache_clear(it->second->ctx); + _chat_prompt.clear(); +} + string Llama::generate(const string &prompt) { - string out; + string out = prompt; - // find the number of tokens in the prompt - int n_prompt = -llama_tokenize(_vocab, prompt.c_str(), prompt.size(), nullptr, 0, true, true); + // ---- tokenize prompt ---- + int n_prompt = -llama_tokenize(_vocab, prompt.c_str(), prompt.size(), + nullptr, 0, true, true); + + if (n_prompt <= 0) { + _last_error = "failed to tokenize prompt"; + return out; + } - // allocate space for the tokens and tokenize the prompt std::vector prompt_tokens(n_prompt); - if (llama_tokenize(_vocab, prompt.c_str(), prompt.size(), prompt_tokens.data(), prompt_tokens.size(), true, true) < 0) { - _last_error = "failed tokenize the prompt"; + if (llama_tokenize(_vocab, prompt.c_str(), prompt.size(), + prompt_tokens.data(), n_prompt, true, true) < 0) { + _last_error = "failed to tokenize prompt"; return out; } - // initialize the sampler + // ---- sampler ---- configure_sampler(); - // prepare a batch for the prompt - llama_batch batch = llama_batch_get_one(prompt_tokens.data(), prompt_tokens.size()); - if (llama_model_has_encoder(_model)) { - if (llama_encode(_ctx, batch)) { - _last_error = "failed to eval"; - return out; - } + // ---- decode prompt ---- + llama_batch batch = llama_batch_get_one(prompt_tokens.data(), n_prompt); + if (llama_decode(_ctx, batch)) { + _last_error = "failed to eval prompt"; + return out; + } + // ---- handle encoder models ---- + if (llama_model_has_encoder(_model)) { llama_token decoder_start_token_id = llama_model_decoder_start_token(_model); if (decoder_start_token_id == LLAMA_TOKEN_NULL) { decoder_start_token_id = llama_vocab_bos(_vocab); } - batch = llama_batch_get_one(&decoder_start_token_id, 1); - } - - for (int n_pos = 0; n_pos + batch.n_tokens < n_prompt + _max_tokens;) { - // evaluate the current batch with the transformer model if (llama_decode(_ctx, batch)) { - _last_error = "failed to eval"; - break; + _last_error = "failed to eval decoder start token"; + return out; } + } + + // ---- generation loop ---- + std::vector decoded; + decoded.reserve(_max_tokens); - n_pos += batch.n_tokens; + int generated = 0; + auto t_start = std::chrono::high_resolution_clock::now(); - // sample the next token - llama_token new_token_id = llama_sampler_sample(_sampler, _ctx, -1); + while (generated < _max_tokens) { + // sample one token from the current logits + llama_token tok = llama_sampler_sample(_sampler, _ctx, -1); - // is it an end of generation? - if (llama_vocab_is_eog(_vocab, new_token_id)) { + // end-of-generation check + if (llama_vocab_is_eog(_vocab, tok)) { break; } - char buf[128]; - int n = llama_token_to_piece(_vocab, new_token_id, buf, sizeof(buf), 0, true); - if (n < 0) { - _last_error = "failed to convert token to piece"; + // append token to decoded list + decoded.push_back(tok); + ++generated; + + // ---- decode the token immediately ---- + llama_batch batch = llama_batch_get_one(&tok, 1); + if (llama_decode(_ctx, batch)) { + _last_error = "failed to eval token during generation"; break; - } else if (n > 0) { - out.append(buf, n); } + } - // prepare the next batch with the sampled token - batch = llama_batch_get_one(&new_token_id, 1); + // ---- detokenize sequentially ---- + if (!decoded.empty()) { + char buf[512]; + for (llama_token tok : decoded) { + if (llama_vocab_is_control(_vocab, tok)) { + continue; + } + int n = llama_token_to_piece(_vocab, tok, buf, sizeof(buf), 0, false); + if (n > 0) { + out.append(buf, n); + } + } } + // ---- timing ---- + auto t_end = std::chrono::high_resolution_clock::now(); + double secs = std::chrono::duration(t_end - t_start).count(); + double tokps = secs > 0 ? generated / secs : 0; + + fprintf(stderr, + "[tok/s=%.2f] generated=%d time=%.3fs\n", + tokps, generated, secs); + return out; } -void Llama::reset() { - // llama_kv_cache_clear(it->second->ctx); - _chat_prompt.clear(); -} diff --git a/llama/llama-sb.h b/llama/llama-sb.h index ccdf881..92921cb 100644 --- a/llama/llama-sb.h +++ b/llama/llama-sb.h @@ -23,6 +23,9 @@ struct Llama { string generate(const string &prompt); // generation parameters + + void set_penalty_last_n(int32_t penalty_last_n) { _penalty_last_n = penalty_last_n; } + void set_penalty_repeat(float penalty_repeat) { _penalty_repeat = penalty_repeat; } void set_max_tokens(int max_tokens) { _max_tokens = max_tokens; } void set_min_p(float min_p) { _min_p = min_p; } void set_temperature(float temperature) { _temperature = temperature; } @@ -49,6 +52,8 @@ struct Llama { const llama_vocab *_vocab; string _chat_prompt; string _last_error; + int32_t _penalty_last_n; + float _penalty_repeat; float _temperature; float _top_p; float _min_p; diff --git a/llama/main.cpp b/llama/main.cpp index 19f3ca4..f8e5261 100644 --- a/llama/main.cpp +++ b/llama/main.cpp @@ -50,6 +50,44 @@ static string expand_path(const char *path) { return result; } + +// +// llama.set_penalty_repeat(0.8) +// +static int cmd_llama_set_penalty_repeat(var_s *self, int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + if (argc != 1) { + error(retval, "llama.set_penalty_repeat", 1, 1); + } else { + int id = get_class_id(self, retval); + if (id != -1) { + Llama &llama = g_map.at(id); + llama.set_penalty_repeat(get_param_num(argc, arg, 0, 0)); + result = 1; + } + } + return result; +} + +// +// llama.set_penalty_last_n(0.8) +// +static int cmd_llama_set_penalty_last_n(var_s *self, int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + if (argc != 1) { + error(retval, "llama.set_penalty_last_n", 1, 1); + } else { + int id = get_class_id(self, retval); + if (id != -1) { + Llama &llama = g_map.at(id); + llama.set_penalty_last_n(get_param_num(argc, arg, 0, 0)); + result = 1; + } + } + return result; +} + + // // llama.set_max_tokens(50) // @@ -105,7 +143,7 @@ static int cmd_llama_set_temperature(var_s *self, int argc, slib_par_t *arg, var } // -// llama.set_set_top_k(10.0) +// llama.set_top_k(10.0) // static int cmd_llama_set_top_k(var_s *self, int argc, slib_par_t *arg, var_s *retval) { int result = 0; @@ -122,6 +160,9 @@ static int cmd_llama_set_top_k(var_s *self, int argc, slib_par_t *arg, var_s *re return result; } +// +// llama.set_top_p(0) +// static int cmd_llama_set_top_p(var_s *self, int argc, slib_par_t *arg, var_s *retval) { int result = 0; if (argc != 1) { @@ -207,14 +248,16 @@ static int cmd_llama_generate(var_s *self, int argc, slib_par_t *arg, var_s *ret static int cmd_create_llama(int argc, slib_par_t *params, var_t *retval) { int result; auto model = expand_path(get_param_str(argc, params, 0, "")); - auto n_ctx = get_param_int(argc, params, 0, 2048); - auto n_batch = get_param_int(argc, params, 1, 1024); - auto temperature = get_param_num(argc, params, 2, 0.25); + auto n_ctx = get_param_int(argc, params, 1, 2048); + auto n_batch = get_param_int(argc, params, 2, 1024); + auto temperature = get_param_num(argc, params, 3, 0.25); int id = ++g_nextId; Llama &llama = g_map[id]; if (llama.construct(model, n_ctx, n_batch)) { llama.set_temperature(temperature); map_init_id(retval, id, CLASS_ID); + v_create_callback(retval, "set_penalty_repeat", cmd_llama_set_penalty_repeat); + v_create_callback(retval, "set_penalty_last_n", cmd_llama_set_penalty_last_n); v_create_callback(retval, "set_max_tokens", cmd_llama_set_max_tokens); v_create_callback(retval, "set_min_p", cmd_llama_set_min_p); v_create_callback(retval, "set_temperature", cmd_llama_set_temperature); diff --git a/llama/samples/chat.bas b/llama/samples/chat.bas index 99d5138..8b2f200 100644 --- a/llama/samples/chat.bas +++ b/llama/samples/chat.bas @@ -1,12 +1,87 @@ +import llm -const llama = llm.llama("qwen.gguf", 1024) +const model = "models/Qwen_Qwen2.5-1.5B-Instruct-GGUF-Q4/qwen2.5-1.5b-instruct-q4_k_m.gguf" +const llama = llm.llama(model, 4096, 512) -print llama.generate("Write a BASIC program", 256, 0.2) +llama.set_max_tokens(150) +llama.set_min_p(0.5) +llama.set_temperature(.8) +llama.set_top_k(1) +llama.set_top_p(0) -print llama.chat("Hello") -print llama.chat("Write a BASIC program to draw a cat") -print llama.chat("Now add color") -llama.reset() +rem factual answers, tools, summaries +' llama.set_max_tokens(150) +' llama.set_temperature(0.0) +' llama.set_top_k(1) +' llama.set_top_p(0.0) +' llama.set_min_p(0.0) -print llama.chat("Who are you?") +rem assistant, Q+A, explanations, chat +' llama.set_max_tokens(150) +' llama.set_temperature(0.8) +' llama.set_top_k(40) +' llama.set_top_p(0.0) +' llama.set_min_p(0.05) + +rem creative, storytelling +' llama.set_max_tokens(200) +' llama.set_temperature(1.0) +' llama.set_top_k(80) +' llama.set_top_p(0.0) +' llama.set_min_p(0.1) + +rem surprises/loko +' llama.set_max_tokens(200) +' llama.set_temperature(1.2) +' llama.set_top_k(120) +' llama.set_top_p(0.0) +' llama.set_min_p(0.15) + +rem technical, conservative +' llama.set_max_tokens(150) +' llama.set_temperature(0.6) +' llama.set_top_k(30) +' llama.set_top_p(0.0) +' llama.set_min_p(0.02) + +rem speed optimised on CPU +llama.set_max_tokens(150) +llama.set_temperature(0.7) +llama.set_top_k(20) +llama.set_top_p(0.0) +llama.set_min_p(0.05) + +' // Conservative - minimal repetition control +' _penalty_last_n = 64; +' _penalty_repeat = 1.05f; + +' // Balanced - good default +' _penalty_last_n = 64; +' _penalty_repeat = 1.1f; + +' // Aggressive - strong anti-repetition +' _penalty_last_n = 128; +' _penalty_repeat = 1.2f; + +' // Disabled +' _penalty_last_n = 0; +' _penalty_repeat = 1.0f; + +llama.set_penalty_repeat(1.15) +llama.set_penalty_last_n(64) + + + +llm_prompt = """\ +you are a helpful assistant\ + \nQuestion: when is dinner?\ +""" + +print llm_prompt +print llama.generate(llm_prompt) + +' iter = llama.generate(llm_prompt) +' while iter != 0 +' print iter.next() +' wend From 059de7536297eb9f0a2e5cb9e6ad49c4f026ff90 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Thu, 25 Dec 2025 07:28:40 +1030 Subject: [PATCH 073/131] LLM: plugin module - generation iterator --- include/param.cpp | 4 +- include/var.h | 6 +- llama/llama-sb.cpp | 62 +++++++-------- llama/llama-sb.h | 22 ++++-- llama/main.cpp | 168 +++++++++++++++++++++++++++-------------- llama/samples/chat.bas | 37 +++++---- llama/test_main.cpp | 12 ++- 7 files changed, 185 insertions(+), 126 deletions(-) diff --git a/include/param.cpp b/include/param.cpp index 135e5af..5a5fea1 100644 --- a/include/param.cpp +++ b/include/param.cpp @@ -589,14 +589,14 @@ void v_create_func(var_p_t map, const char *name, method cb) { var_p_t v_func = map_add_var(map, name, 0); v_func->type = V_FUNC; v_func->v.fn.cb = cb; - v_func->v.fn.mcb = NULL; + v_func->v.fn.mcb = nullptr; v_func->v.fn.id = 0; } void v_create_callback(var_p_t map, const char *name, callback cb) { var_p_t v_func = map_add_var(map, name, 0); v_func->type = V_FUNC; - v_func->v.fn.cb = NULL; + v_func->v.fn.cb = nullptr; v_func->v.fn.mcb = cb; v_func->v.fn.id = 0; } diff --git a/include/var.h b/include/var.h index 16e0f4d..02a5a0a 100644 --- a/include/var.h +++ b/include/var.h @@ -80,7 +80,7 @@ typedef struct var_s { // associative array/map struct { - // pointer the map structure + // pointer to the map structure void *map; uint32_t count; @@ -132,7 +132,7 @@ typedef struct var_s { // non-zero if constant uint8_t const_flag; - // whether help in pooled memory + // whether held in pooled memory uint8_t pooled; } var_t; @@ -154,7 +154,7 @@ var_t *v_new(void); * * @return a newly created var_t array of the given size */ -void v_new_array(var_t *var, unsigned size); +void v_new_array(var_t *var, uint32_t size); /** * @ingroup var diff --git a/llama/llama-sb.cpp b/llama/llama-sb.cpp index 831dc7f..55358b5 100644 --- a/llama/llama-sb.cpp +++ b/llama/llama-sb.cpp @@ -10,6 +10,12 @@ #include "llama.h" #include "llama-sb.h" +LlamaIter::LlamaIter() : + _llama(nullptr), + _tokens_sec(0), + _has_next(false) { +} + Llama::Llama() : _model(nullptr), _ctx(nullptr), @@ -43,18 +49,6 @@ Llama::~Llama() { } } -void Llama::append_response(const string &response) { - _chat_prompt += response; - _chat_prompt += "\n"; -} - -const string Llama::build_chat_prompt(const string &user_msg) { - _chat_prompt += "User: "; - _chat_prompt += user_msg; - _chat_prompt += "\nAssistant: "; - return _chat_prompt; -} - bool Llama::construct(string model_path, int n_ctx, int n_batch) { ggml_backend_load_all(); @@ -107,40 +101,36 @@ void Llama::configure_sampler() { } void Llama::reset() { - // llama_kv_cache_clear(it->second->ctx); + llama_sampler_reset(_sampler); _chat_prompt.clear(); } -string Llama::generate(const string &prompt) { - string out = prompt; - - // ---- tokenize prompt ---- +bool Llama::generate(LlamaIter &iter, const string &prompt) { int n_prompt = -llama_tokenize(_vocab, prompt.c_str(), prompt.size(), nullptr, 0, true, true); if (n_prompt <= 0) { _last_error = "failed to tokenize prompt"; - return out; + return false; } std::vector prompt_tokens(n_prompt); if (llama_tokenize(_vocab, prompt.c_str(), prompt.size(), prompt_tokens.data(), n_prompt, true, true) < 0) { _last_error = "failed to tokenize prompt"; - return out; + return false; } - // ---- sampler ---- configure_sampler(); - // ---- decode prompt ---- + // decode prompt llama_batch batch = llama_batch_get_one(prompt_tokens.data(), n_prompt); if (llama_decode(_ctx, batch)) { _last_error = "failed to eval prompt"; - return out; + return false; } - // ---- handle encoder models ---- + // handle encoder models if (llama_model_has_encoder(_model)) { llama_token decoder_start_token_id = llama_model_decoder_start_token(_model); if (decoder_start_token_id == LLAMA_TOKEN_NULL) { @@ -149,11 +139,20 @@ string Llama::generate(const string &prompt) { batch = llama_batch_get_one(&decoder_start_token_id, 1); if (llama_decode(_ctx, batch)) { _last_error = "failed to eval decoder start token"; - return out; + return false; } } - // ---- generation loop ---- + iter._llama = this; + iter._batch = batch; + iter._has_next = true; + iter._tokens_sec = 0; + return true; +} + +string Llama::next(LlamaIter &iter) { + string out; + std::vector decoded; decoded.reserve(_max_tokens); @@ -166,6 +165,7 @@ string Llama::generate(const string &prompt) { // end-of-generation check if (llama_vocab_is_eog(_vocab, tok)) { + iter._has_next = false; break; } @@ -173,7 +173,7 @@ string Llama::generate(const string &prompt) { decoded.push_back(tok); ++generated; - // ---- decode the token immediately ---- + // decode the token llama_batch batch = llama_batch_get_one(&tok, 1); if (llama_decode(_ctx, batch)) { _last_error = "failed to eval token during generation"; @@ -181,7 +181,7 @@ string Llama::generate(const string &prompt) { } } - // ---- detokenize sequentially ---- + // detokenize sequentially if (!decoded.empty()) { char buf[512]; for (llama_token tok : decoded) { @@ -195,14 +195,10 @@ string Llama::generate(const string &prompt) { } } - // ---- timing ---- + // timing auto t_end = std::chrono::high_resolution_clock::now(); double secs = std::chrono::duration(t_end - t_start).count(); - double tokps = secs > 0 ? generated / secs : 0; - - fprintf(stderr, - "[tok/s=%.2f] generated=%d time=%.3fs\n", - tokps, generated, secs); + iter._tokens_sec = secs > 0 ? generated / secs : 0; return out; } diff --git a/llama/llama-sb.h b/llama/llama-sb.h index 92921cb..78b3955 100644 --- a/llama/llama-sb.h +++ b/llama/llama-sb.h @@ -12,6 +12,18 @@ using namespace std; +struct Llama; + +struct LlamaIter { + explicit LlamaIter(); + ~LlamaIter() {} + + Llama *_llama; + llama_batch _batch; + float _tokens_sec; + bool _has_next; +}; + struct Llama { explicit Llama(); ~Llama(); @@ -20,10 +32,10 @@ struct Llama { bool construct(string model_path, int n_ctx, int n_batch); // generation - string generate(const string &prompt); + bool generate(LlamaIter &iter, const string &prompt); + string next(LlamaIter &iter); // generation parameters - void set_penalty_last_n(int32_t penalty_last_n) { _penalty_last_n = penalty_last_n; } void set_penalty_repeat(float penalty_repeat) { _penalty_repeat = penalty_repeat; } void set_max_tokens(int max_tokens) { _max_tokens = max_tokens; } @@ -32,12 +44,6 @@ struct Llama { void set_top_k(int top_k) { _top_k = top_k; } void set_top_p(float top_p) { _top_p = top_p; } - // messages - void append_response(const string &response); - void append_user_message(const string &user_msg); - const string& get_chat_history() const; - const string build_chat_prompt(const string &user_msg); - // error handling const char *last_error() { return _last_error.c_str(); } void set_log_level(int level) { _log_level = level; } diff --git a/llama/main.cpp b/llama/main.cpp index f8e5261..552b689 100644 --- a/llama/main.cpp +++ b/llama/main.cpp @@ -15,16 +15,18 @@ #include "llama-sb.h" -#define CLASS_ID 1 +#define CLASS_ID_LLAMA 1 +#define CLASS_ID_LLAMA_ITER 2 int g_nextId = 1; -robin_hood::unordered_map g_map; +robin_hood::unordered_map g_llama; +robin_hood::unordered_map g_llama_iter; -static int get_class_id(var_s *map, var_s *retval) { +static int get_llama_class_id(var_s *map, var_s *retval) { int result = -1; if (is_map(map)) { int id = map->v.m.id; - if (id != -1 && g_map.find(id) != g_map.end()) { + if (id != -1 && g_llama.find(id) != g_llama.end()) { result = id; } } @@ -34,6 +36,20 @@ static int get_class_id(var_s *map, var_s *retval) { return result; } +static int get_llama_iter_class_id(var_s *map, var_s *retval) { + int result = -1; + if (is_map(map)) { + int id = map->v.m.id; + if (id != -1 && g_llama_iter.find(id) != g_llama_iter.end()) { + result = id; + } + } + if (result == -1) { + error(retval, "Llama iter not found"); + } + return result; +} + static string expand_path(const char *path) { string result; if (path && path[0] == '~') { @@ -59,9 +75,9 @@ static int cmd_llama_set_penalty_repeat(var_s *self, int argc, slib_par_t *arg, if (argc != 1) { error(retval, "llama.set_penalty_repeat", 1, 1); } else { - int id = get_class_id(self, retval); + int id = get_llama_class_id(self, retval); if (id != -1) { - Llama &llama = g_map.at(id); + Llama &llama = g_llama.at(id); llama.set_penalty_repeat(get_param_num(argc, arg, 0, 0)); result = 1; } @@ -77,9 +93,9 @@ static int cmd_llama_set_penalty_last_n(var_s *self, int argc, slib_par_t *arg, if (argc != 1) { error(retval, "llama.set_penalty_last_n", 1, 1); } else { - int id = get_class_id(self, retval); + int id = get_llama_class_id(self, retval); if (id != -1) { - Llama &llama = g_map.at(id); + Llama &llama = g_llama.at(id); llama.set_penalty_last_n(get_param_num(argc, arg, 0, 0)); result = 1; } @@ -96,9 +112,9 @@ static int cmd_llama_set_max_tokens(var_s *self, int argc, slib_par_t *arg, var_ if (argc != 1) { error(retval, "llama.set_max_tokens", 1, 1); } else { - int id = get_class_id(self, retval); + int id = get_llama_class_id(self, retval); if (id != -1) { - Llama &llama = g_map.at(id); + Llama &llama = g_llama.at(id); llama.set_max_tokens(get_param_int(argc, arg, 0, 0)); result = 1; } @@ -114,9 +130,9 @@ static int cmd_llama_set_min_p(var_s *self, int argc, slib_par_t *arg, var_s *re if (argc != 1) { error(retval, "llama.set_min_p", 1, 1); } else { - int id = get_class_id(self, retval); + int id = get_llama_class_id(self, retval); if (id != -1) { - Llama &llama = g_map.at(id); + Llama &llama = g_llama.at(id); llama.set_min_p(get_param_num(argc, arg, 0, 0)); result = 1; } @@ -132,9 +148,9 @@ static int cmd_llama_set_temperature(var_s *self, int argc, slib_par_t *arg, var if (argc != 1) { error(retval, "llama.set_temperature", 1, 1); } else { - int id = get_class_id(self, retval); + int id = get_llama_class_id(self, retval); if (id != -1) { - Llama &llama = g_map.at(id); + Llama &llama = g_llama.at(id); llama.set_temperature(get_param_num(argc, arg, 0, 0)); result = 1; } @@ -150,9 +166,9 @@ static int cmd_llama_set_top_k(var_s *self, int argc, slib_par_t *arg, var_s *re if (argc != 1) { error(retval, "llama.set_top_k", 1, 1); } else { - int id = get_class_id(self, retval); + int id = get_llama_class_id(self, retval); if (id != -1) { - Llama &llama = g_map.at(id); + Llama &llama = g_llama.at(id); llama.set_top_k(get_param_int(argc, arg, 0, 0)); result = 1; } @@ -168,9 +184,9 @@ static int cmd_llama_set_top_p(var_s *self, int argc, slib_par_t *arg, var_s *re if (argc != 1) { error(retval, "llama.set_top_p", 1, 1); } else { - int id = get_class_id(self, retval); + int id = get_llama_class_id(self, retval); if (id != -1) { - Llama &llama = g_map.at(id); + Llama &llama = g_llama.at(id); llama.set_top_p(get_param_num(argc, arg, 0, 0)); result = 1; } @@ -179,28 +195,54 @@ static int cmd_llama_set_top_p(var_s *self, int argc, slib_par_t *arg, var_s *re } // -// print llama.chat("Hello") +// llama.reset() - make the model forget everything // -static int cmd_llama_chat(var_s *self, int argc, slib_par_t *arg, var_s *retval) { +static int cmd_llama_reset(var_s *self, int argc, slib_par_t *arg, var_s *retval) { int result = 0; - if (argc != 1) { - error(retval, "llama.chat", 1, 1); + if (argc != 0) { + error(retval, "llama.reset", 0, 0); } else { - int id = get_class_id(self, retval); + int id = get_llama_class_id(self, retval); if (id != -1) { - Llama &llama = g_map.at(id); - auto prompt = get_param_str(argc, arg, 0, ""); - - // build accumulated prompt - string updated_prompt = llama.build_chat_prompt(prompt); - - // run generation WITHOUT clearing cache - string response = llama.generate(updated_prompt); + Llama &llama = g_llama.at(id); + llama.reset(); + result = 1; + } + } + return result; +} - // append assistant reply to history - llama.append_response(response); +// +// iter.has_next() +// +static int cmd_llama_has_next(var_s *self, int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + if (argc != 0) { + error(retval, "iter.has_next", 0, 0); + } else { + int id = get_llama_iter_class_id(self, retval); + if (id != -1) { + LlamaIter &llamaIter = g_llama_iter.at(id); + v_setint(retval, llamaIter._has_next); + result = 1; + } + } + return result; +} - v_setstr(retval, response.c_str()); +// +// iter.next() +// +static int cmd_llama_next(var_s *self, int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + if (argc != 0) { + error(retval, "iter.next", 0, 0); + } else { + int id = get_llama_iter_class_id(self, retval); + if (id != -1) { + LlamaIter &iter = g_llama_iter.at(id); + auto out = iter._llama->next(iter); + v_setstr(retval, out.c_str()); result = 1; } } @@ -208,17 +250,17 @@ static int cmd_llama_chat(var_s *self, int argc, slib_par_t *arg, var_s *retval) } // -// llama.reset() - make the model forget everything +// iter.tokens_sec // -static int cmd_llama_reset(var_s *self, int argc, slib_par_t *arg, var_s *retval) { +static int cmd_llama_tokens_sec(var_s *self, int argc, slib_par_t *arg, var_s *retval) { int result = 0; if (argc != 0) { - error(retval, "llama.reset", 0, 0); + error(retval, "iter.tokens_sec", 0, 0); } else { - int id = get_class_id(self, retval); + int id = get_llama_iter_class_id(self, retval); if (id != -1) { - Llama &llama = g_map.at(id); - llama.reset(); + LlamaIter &llamaIter = g_llama_iter.at(id); + v_setreal(retval, llamaIter._tokens_sec); result = 1; } } @@ -233,13 +275,21 @@ static int cmd_llama_generate(var_s *self, int argc, slib_par_t *arg, var_s *ret if (argc != 1) { error(retval, "llama.generate", 1, 1); } else { - int id = get_class_id(self, retval); + int id = get_llama_class_id(self, retval); if (id != -1) { - Llama &llama = g_map.at(id); + int iter_id = ++g_nextId; + LlamaIter &iter = g_llama_iter[iter_id]; + Llama &llama = g_llama.at(id); auto prompt = get_param_str(argc, arg, 0, ""); - string response = llama.generate(prompt); - v_setstr(retval, response.c_str()); - result = 1; + if (llama.generate(iter, prompt)) { + map_init_id(retval, iter_id, CLASS_ID_LLAMA_ITER); + v_create_callback(retval, "has_next", cmd_llama_has_next); + v_create_callback(retval, "next", cmd_llama_next); + v_create_callback(retval, "tokens_sec", cmd_llama_tokens_sec); + result = 1; + } else { + error(retval, llama.last_error()); + } } } return result; @@ -250,12 +300,10 @@ static int cmd_create_llama(int argc, slib_par_t *params, var_t *retval) { auto model = expand_path(get_param_str(argc, params, 0, "")); auto n_ctx = get_param_int(argc, params, 1, 2048); auto n_batch = get_param_int(argc, params, 2, 1024); - auto temperature = get_param_num(argc, params, 3, 0.25); int id = ++g_nextId; - Llama &llama = g_map[id]; + Llama &llama = g_llama[id]; if (llama.construct(model, n_ctx, n_batch)) { - llama.set_temperature(temperature); - map_init_id(retval, id, CLASS_ID); + map_init_id(retval, id, CLASS_ID_LLAMA); v_create_callback(retval, "set_penalty_repeat", cmd_llama_set_penalty_repeat); v_create_callback(retval, "set_penalty_last_n", cmd_llama_set_penalty_last_n); v_create_callback(retval, "set_max_tokens", cmd_llama_set_max_tokens); @@ -263,13 +311,12 @@ static int cmd_create_llama(int argc, slib_par_t *params, var_t *retval) { v_create_callback(retval, "set_temperature", cmd_llama_set_temperature); v_create_callback(retval, "set_top_k", cmd_llama_set_top_k); v_create_callback(retval, "set_top_p", cmd_llama_set_top_p); - v_create_callback(retval, "chat", cmd_llama_chat); v_create_callback(retval, "generate", cmd_llama_generate); v_create_callback(retval, "reset", cmd_llama_reset); result = 1; } else { error(retval, llama.last_error()); - g_map.erase(id); + g_llama.erase(id); result = 0; } return result; @@ -302,9 +349,14 @@ int sblib_init(const char *sourceFile) { SBLIB_API void sblib_free(int cls_id, int id) { if (id != -1) { switch (cls_id) { - case CLASS_ID: - if (g_map.find(id) != g_map.end()) { - g_map.erase(id); + case CLASS_ID_LLAMA: + if (g_llama.find(id) != g_llama.end()) { + g_llama.erase(id); + } + break; + case CLASS_ID_LLAMA_ITER: + if (g_llama_iter.find(id) != g_llama_iter.end()) { + g_llama_iter.erase(id); } break; } @@ -315,9 +367,13 @@ SBLIB_API void sblib_free(int cls_id, int id) { // Program termination // void sblib_close(void) { - if (!g_map.empty()) { + if (!g_llama.empty()) { fprintf(stderr, "LLM leak detected\n"); - g_map.clear(); + g_llama.clear(); + } + if (!g_llama_iter.empty()) { + fprintf(stderr, "LLM iter leak detected\n"); + g_llama_iter.clear(); } } diff --git a/llama/samples/chat.bas b/llama/samples/chat.bas index 8b2f200..8fd68c9 100644 --- a/llama/samples/chat.bas +++ b/llama/samples/chat.bas @@ -3,12 +3,11 @@ import llm const model = "models/Qwen_Qwen2.5-1.5B-Instruct-GGUF-Q4/qwen2.5-1.5b-instruct-q4_k_m.gguf" const llama = llm.llama(model, 4096, 512) -llama.set_max_tokens(150) -llama.set_min_p(0.5) -llama.set_temperature(.8) -llama.set_top_k(1) -llama.set_top_p(0) - +' llama.set_max_tokens(150) +' llama.set_min_p(0.5) +' llama.set_temperature(.8) +' llama.set_top_k(1) +' llama.set_top_p(0) rem factual answers, tools, summaries ' llama.set_max_tokens(150) @@ -46,7 +45,7 @@ rem technical, conservative ' llama.set_min_p(0.02) rem speed optimised on CPU -llama.set_max_tokens(150) +llama.set_max_tokens(10) llama.set_temperature(0.7) llama.set_top_k(20) llama.set_top_p(0.0) @@ -68,20 +67,18 @@ llama.set_min_p(0.05) ' _penalty_last_n = 0; ' _penalty_repeat = 1.0f; -llama.set_penalty_repeat(1.15) -llama.set_penalty_last_n(64) - +' llama.set_penalty_repeat(1.15) +' llama.set_penalty_last_n(64) - -llm_prompt = """\ +prompt = """\ you are a helpful assistant\ \nQuestion: when is dinner?\ """ - -print llm_prompt -print llama.generate(llm_prompt) - -' iter = llama.generate(llm_prompt) -' while iter != 0 -' print iter.next() -' wend +print prompt +print "============" +iter = llama.generate(prompt) +while iter.has_next() + print iter.next() +wend +print "============" +print iter.tokens_sec() diff --git a/llama/test_main.cpp b/llama/test_main.cpp index 8900f94..c24708f 100644 --- a/llama/test_main.cpp +++ b/llama/test_main.cpp @@ -57,10 +57,14 @@ int main(int argc, char ** argv) { Llama llama; if (llama.construct(model_path, 1024, 1024)) { - string out = llama.generate(prompt); - printf("\033[33m"); - printf(out.c_str()); - printf("\n\033[0m"); + LlamaIter iter; + llama.generate(iter, prompt); + while (iter._has_next) { + auto out = llama.next(iter); + printf("\033[33m"); + printf(out.c_str()); + printf("\n\033[0m"); + } } else { fprintf(stderr, "ERR: %s\n", llama.last_error()); } From fad622ea163e63f83675700dd356b06c81577236 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Thu, 25 Dec 2025 17:09:48 +1030 Subject: [PATCH 074/131] LLM: plugin module - impl add_stop func --- llama/llama-sb.cpp | 96 +++++++++++++++++++++++++++++++--------------- llama/llama-sb.h | 9 ++++- llama/main.cpp | 22 ++++++++++- 3 files changed, 92 insertions(+), 35 deletions(-) diff --git a/llama/llama-sb.cpp b/llama/llama-sb.cpp index 55358b5..9d5654e 100644 --- a/llama/llama-sb.cpp +++ b/llama/llama-sb.cpp @@ -6,13 +6,15 @@ // Copyright(C) 2026 Chris Warren-Smith #include -#include #include "llama.h" #include "llama-sb.h" +constexpr int MAX_REPEAT = 5; + LlamaIter::LlamaIter() : _llama(nullptr), _tokens_sec(0), + _repetition_count(0), _has_next(false) { } @@ -21,13 +23,13 @@ Llama::Llama() : _ctx(nullptr), _sampler(nullptr), _vocab(nullptr), - _penalty_last_n(64), - _penalty_repeat(1.1f), + _penalty_last_n(0), + _penalty_repeat(0), _temperature(0), _top_k(0), - _top_p(1.0f), - _min_p(0.0f), - _max_tokens(150), + _top_p(0), + _min_p(0), + _max_tokens(0), _log_level(GGML_LOG_LEVEL_CONT) { llama_log_set([](enum ggml_log_level level, const char * text, void *user_data) { Llama *llama = (Llama *)user_data; @@ -35,6 +37,7 @@ Llama::Llama() : fprintf(stderr, "LLAMA: %s", text); } }, this); + reset(); } Llama::~Llama() { @@ -49,6 +52,18 @@ Llama::~Llama() { } } +void Llama::reset() { + _stop_sequences.clear(); + _last_error = ""; + _penalty_last_n = 64; + _penalty_repeat = 1.1f; + _temperature = 0; + _top_k = 0; + _top_p = 1.0f; + _min_p = 0.0f; + _max_tokens = 150; +} + bool Llama::construct(string model_path, int n_ctx, int n_batch) { ggml_backend_load_all(); @@ -100,31 +115,31 @@ void Llama::configure_sampler() { } } -void Llama::reset() { - llama_sampler_reset(_sampler); - _chat_prompt.clear(); -} - -bool Llama::generate(LlamaIter &iter, const string &prompt) { - int n_prompt = -llama_tokenize(_vocab, prompt.c_str(), prompt.size(), - nullptr, 0, true, true); +vector Llama::tokenize(const string &prompt) { + vector result; + int n_prompt = -llama_tokenize(_vocab, prompt.c_str(), prompt.size(), nullptr, 0, true, true); if (n_prompt <= 0) { _last_error = "failed to tokenize prompt"; - return false; + } else { + result.reserve(n_prompt); + result.resize(n_prompt); + if (llama_tokenize(_vocab, prompt.c_str(), prompt.size(), + result.data(), n_prompt, true, true) < 0) { + _last_error = "failed to tokenize prompt"; + } } + return result; +} - std::vector prompt_tokens(n_prompt); - if (llama_tokenize(_vocab, prompt.c_str(), prompt.size(), - prompt_tokens.data(), n_prompt, true, true) < 0) { - _last_error = "failed to tokenize prompt"; +bool Llama::generate(LlamaIter &iter, const string &prompt) { + vector prompt_tokens = tokenize(prompt); + if (prompt_tokens.size() == 0) { return false; } - configure_sampler(); - // decode prompt - llama_batch batch = llama_batch_get_one(prompt_tokens.data(), n_prompt); + llama_batch batch = llama_batch_get_one(prompt_tokens.data(), prompt_tokens.size()); if (llama_decode(_ctx, batch)) { _last_error = "failed to eval prompt"; return false; @@ -143,8 +158,9 @@ bool Llama::generate(LlamaIter &iter, const string &prompt) { } } + configure_sampler(); + iter._llama = this; - iter._batch = batch; iter._has_next = true; iter._tokens_sec = 0; return true; @@ -153,7 +169,7 @@ bool Llama::generate(LlamaIter &iter, const string &prompt) { string Llama::next(LlamaIter &iter) { string out; - std::vector decoded; + vector decoded; decoded.reserve(_max_tokens); int generated = 0; @@ -183,14 +199,32 @@ string Llama::next(LlamaIter &iter) { // detokenize sequentially if (!decoded.empty()) { - char buf[512]; for (llama_token tok : decoded) { - if (llama_vocab_is_control(_vocab, tok)) { - continue; - } - int n = llama_token_to_piece(_vocab, tok, buf, sizeof(buf), 0, false); - if (n > 0) { - out.append(buf, n); + if (!llama_vocab_is_control(_vocab, tok)) { + char buf[512]; + int n = llama_token_to_piece(_vocab, tok, buf, sizeof(buf), 0, false); + if (n > 0) { + if (iter._last_word == buf) { + if (++iter._repetition_count == MAX_REPEAT) { + iter._has_next = false; + break; + } + } else { + iter._repetition_count = 0; + iter._last_word = buf; + } + out.append(buf, n); + + for (const auto &stop : _stop_sequences) { + size_t pos = out.find(stop); + if (pos != std::string::npos) { + // found stop sequence - truncate and signal end + out = out.substr(0, pos); + iter._has_next = false; + break; + } + } + } } } } diff --git a/llama/llama-sb.h b/llama/llama-sb.h index 78b3955..6cc807d 100644 --- a/llama/llama-sb.h +++ b/llama/llama-sb.h @@ -8,6 +8,7 @@ #pragma once #include +#include #include "llama.h" using namespace std; @@ -19,8 +20,9 @@ struct LlamaIter { ~LlamaIter() {} Llama *_llama; - llama_batch _batch; float _tokens_sec; + string _last_word; + int _repetition_count; bool _has_next; }; @@ -36,6 +38,8 @@ struct Llama { string next(LlamaIter &iter); // generation parameters + void add_stop(const char *stop) { _stop_sequences.push_back(stop); } + void clear_stops() { _stop_sequences.clear(); } void set_penalty_last_n(int32_t penalty_last_n) { _penalty_last_n = penalty_last_n; } void set_penalty_repeat(float penalty_repeat) { _penalty_repeat = penalty_repeat; } void set_max_tokens(int max_tokens) { _max_tokens = max_tokens; } @@ -51,12 +55,13 @@ struct Llama { private: void configure_sampler(); + vector tokenize(const string &prompt); llama_model *_model; llama_context *_ctx; llama_sampler *_sampler; const llama_vocab *_vocab; - string _chat_prompt; + vector _stop_sequences; string _last_error; int32_t _penalty_last_n; float _penalty_repeat; diff --git a/llama/main.cpp b/llama/main.cpp index 552b689..f93aa62 100644 --- a/llama/main.cpp +++ b/llama/main.cpp @@ -66,6 +66,23 @@ static string expand_path(const char *path) { return result; } +// +// llama.add_stop('xyz') +// +static int cmd_llama_add_stop(var_s *self, int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + if (argc != 1) { + error(retval, "llama.add_stop", 1, 1); + } else { + int id = get_llama_class_id(self, retval); + if (id != -1) { + Llama &llama = g_llama.at(id); + llama.add_stop(get_param_str(argc, arg, 0, "stop")); + result = 1; + } + } + return result; +} // // llama.set_penalty_repeat(0.8) @@ -304,6 +321,9 @@ static int cmd_create_llama(int argc, slib_par_t *params, var_t *retval) { Llama &llama = g_llama[id]; if (llama.construct(model, n_ctx, n_batch)) { map_init_id(retval, id, CLASS_ID_LLAMA); + v_create_callback(retval, "add_stop", cmd_llama_add_stop); + v_create_callback(retval, "generate", cmd_llama_generate); + v_create_callback(retval, "reset", cmd_llama_reset); v_create_callback(retval, "set_penalty_repeat", cmd_llama_set_penalty_repeat); v_create_callback(retval, "set_penalty_last_n", cmd_llama_set_penalty_last_n); v_create_callback(retval, "set_max_tokens", cmd_llama_set_max_tokens); @@ -311,8 +331,6 @@ static int cmd_create_llama(int argc, slib_par_t *params, var_t *retval) { v_create_callback(retval, "set_temperature", cmd_llama_set_temperature); v_create_callback(retval, "set_top_k", cmd_llama_set_top_k); v_create_callback(retval, "set_top_p", cmd_llama_set_top_p); - v_create_callback(retval, "generate", cmd_llama_generate); - v_create_callback(retval, "reset", cmd_llama_reset); result = 1; } else { error(retval, llama.last_error()); From 67db281e57285f6ac94eaa8a869e2bb55c775632 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Sat, 27 Dec 2025 18:47:08 +1030 Subject: [PATCH 075/131] LLM: plugin module - fix iteration issues --- llama/CMakeLists.txt | 4 +- llama/README.md | 91 ++++++++++++++ llama/llama-sb.cpp | 245 +++++++++++++++++++++++++----------- llama/llama-sb.h | 6 +- llama/llama.cpp | 2 +- llama/main.cpp | 7 +- llama/samples/adventure.bas | 117 +++++++++++++++++ llama/test_main.cpp | 2 +- 8 files changed, 397 insertions(+), 77 deletions(-) create mode 100644 llama/README.md create mode 100644 llama/samples/adventure.bas diff --git a/llama/CMakeLists.txt b/llama/CMakeLists.txt index aad1e62..a7e54d1 100644 --- a/llama/CMakeLists.txt +++ b/llama/CMakeLists.txt @@ -1,7 +1,9 @@ cmake_minimum_required(VERSION 3.15) project(llm C CXX) -set(CMAKE_CXX_STANDARD 17) +# clang-check ../*.cpp +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +set(CMAKE_CXX_STANDARD 20) set(CMAKE_C_STANDARD 11) # ----------------------------- diff --git a/llama/README.md b/llama/README.md new file mode 100644 index 0000000..e6ed506 --- /dev/null +++ b/llama/README.md @@ -0,0 +1,91 @@ +# Generator settings + +## factual answers, tools, summaries + +``` +llama.set_max_tokens(150) +llama.set_temperature(0.0) +llama.set_top_k(1) +llama.set_top_p(0.0) +llama.set_min_p(0.0) +``` + +## assistant, Q+A, explanations, chat + +``` +llama.set_max_tokens(150) +llama.set_temperature(0.8) +llama.set_top_k(40) +llama.set_top_p(0.0) +llama.set_min_p(0.05) +``` + +## creative, storytelling + +``` +llama.set_max_tokens(20) +llama.set_temperature(1.0) +llama.set_top_k(80) +llama.set_top_p(0.0) +llama.set_min_p(0.1) +``` + +## surprises + +``` +llama.set_max_tokens(200) +llama.set_temperature(1.2) +llama.set_top_k(120) +llama.set_top_p(0.0) +llama.set_min_p(0.15) +``` + +## technical, conservative + +``` +llama.set_max_tokens(150) +llama.set_temperature(0.6) +llama.set_top_k(30) +llama.set_top_p(0.0) +llama.set_min_p(0.02) +``` + +## speed optimised on CPU + +``` +' llama.set_max_tokens(10) +' llama.set_temperature(0.7) +' llama.set_top_k(20) +' llama.set_top_p(0.0) +' llama.set_min_p(0.05) +``` + +# Avoiding repetition + +## Conservative - minimal repetition control + +``` +llama.set_penalty_last_n(64) +llama.set_penalty_repeat(1.05) +``` + +## Balanced - good default + +``` +set_penalty_last_n(64) +set_penalty_repeat(1.1) +``` + +## Aggressive - strong anti-repetition + +``` +set_penalty_last_n(128) +set_penalty_repeat(1.2) +``` + +## Disabled + +``` +llama.set_penalty_last_n(0) +llama.set_penalty_repeat(1.0) +``` diff --git a/llama/llama-sb.cpp b/llama/llama-sb.cpp index 9d5654e..0e5e57f 100644 --- a/llama/llama-sb.cpp +++ b/llama/llama-sb.cpp @@ -5,7 +5,8 @@ // // Copyright(C) 2026 Chris Warren-Smith -#include +#include +#include #include "llama.h" #include "llama-sb.h" @@ -13,8 +14,8 @@ constexpr int MAX_REPEAT = 5; LlamaIter::LlamaIter() : _llama(nullptr), - _tokens_sec(0), _repetition_count(0), + _tokens_generated(0), _has_next(false) { } @@ -26,9 +27,9 @@ Llama::Llama() : _penalty_last_n(0), _penalty_repeat(0), _temperature(0), - _top_k(0), _top_p(0), _min_p(0), + _top_k(0), _max_tokens(0), _log_level(GGML_LOG_LEVEL_CONT) { llama_log_set([](enum ggml_log_level level, const char * text, void *user_data) { @@ -72,15 +73,19 @@ bool Llama::construct(string model_path, int n_ctx, int n_batch) { _model = llama_model_load_from_file(model_path.c_str(), mparams); if (!_model) { - _last_error = "failed to load model"; + _last_error = "Failed to load model"; } else { llama_context_params cparams = llama_context_default_params(); cparams.n_ctx = n_ctx; cparams.n_batch = n_batch; + cparams.n_ubatch = n_batch; cparams.no_perf = true; + cparams.attention_type = LLAMA_ATTENTION_TYPE_UNSPECIFIED; + cparams.flash_attn_type = LLAMA_FLASH_ATTN_TYPE_AUTO; + _ctx = llama_init_from_model(_model, cparams); if (!_ctx) { - _last_error = "failed to create context"; + _last_error = "Failed to create context"; } else { _vocab = llama_model_get_vocab(_model); @@ -120,120 +125,218 @@ vector Llama::tokenize(const string &prompt) { int n_prompt = -llama_tokenize(_vocab, prompt.c_str(), prompt.size(), nullptr, 0, true, true); if (n_prompt <= 0) { - _last_error = "failed to tokenize prompt"; + _last_error = "Failed to tokenize prompt"; } else { result.reserve(n_prompt); result.resize(n_prompt); if (llama_tokenize(_vocab, prompt.c_str(), prompt.size(), result.data(), n_prompt, true, true) < 0) { - _last_error = "failed to tokenize prompt"; + _last_error = "Failed to tokenize prompt"; } } return result; } +// Makes space in the context for n_tokens by removing old tokens if necessary +// Returns true if successful, false if impossible to make space +// +// Strategies: +// - If enough space exists, does nothing +// - If n_tokens > n_ctx, fails (impossible to fit) +// - Otherwise, removes oldest tokens to make room +// +// Parameters: +// n_tokens - Number of tokens we need space for +// keep_min - Minimum tokens to keep (e.g., system prompt), default 0 +// +bool Llama::make_space_for_tokens(int n_tokens, int keep_min) { + int n_ctx = llama_n_ctx(_ctx); + if (n_tokens > n_ctx) { + _last_error = "Too many tokens, increase context size (n_ctx)"; + return false; + } + + llama_memory_t mem = llama_get_memory(_ctx); + + // Get current position range + llama_pos pos_min = llama_memory_seq_pos_min(mem, 0); + llama_pos pos_max = llama_memory_seq_pos_max(mem, 0); + + // Empty memory - nothing to do + if (pos_max < 0) { + return true; + } + + int current_used = pos_max - pos_min + 1; + int space_needed = n_tokens; + int space_available = n_ctx - current_used; + + // Already have enough space + if (space_available >= space_needed) { + return true; + } + + // Calculate how many tokens to remove + int tokens_to_remove = space_needed - space_available; + + // Can't remove more than we have (minus keep_min) + int removable = current_used - keep_min; + if (tokens_to_remove > removable) { + _last_error = "Can't make enough space while keeping keep_min tokens"; + return false; + } + + // Remove oldest tokens (from pos_min to pos_min + tokens_to_remove) + llama_memory_seq_rm(mem, 0, pos_min, pos_min + tokens_to_remove); + + // Shift remaining tokens down + llama_memory_seq_add(mem, 0, pos_min + tokens_to_remove, -1, -tokens_to_remove); + + return true; +} + bool Llama::generate(LlamaIter &iter, const string &prompt) { vector prompt_tokens = tokenize(prompt); if (prompt_tokens.size() == 0) { return false; } - // decode prompt - llama_batch batch = llama_batch_get_one(prompt_tokens.data(), prompt_tokens.size()); - if (llama_decode(_ctx, batch)) { - _last_error = "failed to eval prompt"; + if (!make_space_for_tokens(prompt_tokens.size(), 0)) { return false; } + // batch decode tokens + uint32_t n_batch = llama_n_batch(_ctx); + for (size_t i = 0; i < prompt_tokens.size(); i += n_batch) { + size_t batch_size = std::min((size_t)n_batch, prompt_tokens.size() - i); + llama_batch batch = llama_batch_get_one(prompt_tokens.data() + i, batch_size); + int result = llama_decode(_ctx, batch); + if (result != 0) { + _last_error = std::format("Failed to decode batch. position:{} error:{}", i, result); + return false; + } + } + // handle encoder models if (llama_model_has_encoder(_model)) { + // for example: T5, BART, and mBART. + // Used for translation, summarization, text-to-text, paraphrasing, question answering llama_token decoder_start_token_id = llama_model_decoder_start_token(_model); if (decoder_start_token_id == LLAMA_TOKEN_NULL) { decoder_start_token_id = llama_vocab_bos(_vocab); } - batch = llama_batch_get_one(&decoder_start_token_id, 1); - if (llama_decode(_ctx, batch)) { - _last_error = "failed to eval decoder start token"; + + llama_batch decoder_batch = llama_batch_get_one(&decoder_start_token_id, 1); + if (llama_decode(_ctx, decoder_batch)) { + _last_error = "Failed to evaluate decoder start token"; return false; } } configure_sampler(); + iter._t_start = std::chrono::high_resolution_clock::now(); iter._llama = this; iter._has_next = true; - iter._tokens_sec = 0; return true; } -string Llama::next(LlamaIter &iter) { - string out; +bool Llama::ends_with_sentence_boundary(const string &text) { + if (text.empty()) { + return false; + } - vector decoded; - decoded.reserve(_max_tokens); + // Get last few characters (in case of whitespace after punctuation) + size_t check_len = std::min(text.length(), (size_t)5); + std::string ending = text.substr(text.length() - check_len); - int generated = 0; - auto t_start = std::chrono::high_resolution_clock::now(); + // Check for various sentence endings + // Period followed by space or end + if (ending.find(". ") != std::string::npos || + ending.back() == '.') { + return true; + } - while (generated < _max_tokens) { - // sample one token from the current logits - llama_token tok = llama_sampler_sample(_sampler, _ctx, -1); + // Exclamation mark + if (ending.find("! ") != std::string::npos || + ending.back() == '!') { + return true; + } - // end-of-generation check - if (llama_vocab_is_eog(_vocab, tok)) { - iter._has_next = false; - break; - } + // Question mark + if (ending.find("? ") != std::string::npos || + ending.back() == '?') { + return true; + } - // append token to decoded list - decoded.push_back(tok); - ++generated; + // Newline (paragraph break) + if (ending.find('\n') != std::string::npos) { + return true; + } - // decode the token - llama_batch batch = llama_batch_get_one(&tok, 1); - if (llama_decode(_ctx, batch)) { - _last_error = "failed to eval token during generation"; - break; - } + // Quote followed by period: "something." + if (ending.find(".\"") != std::string::npos || + ending.find("!\"") != std::string::npos || + ending.find("?\"") != std::string::npos) { + return true; + } + + return false; +} + +string Llama::next(LlamaIter &iter) { + if (!iter._has_next) { + _last_error = "Iteration beyond end of stream"; + return ""; } - // detokenize sequentially - if (!decoded.empty()) { - for (llama_token tok : decoded) { - if (!llama_vocab_is_control(_vocab, tok)) { - char buf[512]; - int n = llama_token_to_piece(_vocab, tok, buf, sizeof(buf), 0, false); - if (n > 0) { - if (iter._last_word == buf) { - if (++iter._repetition_count == MAX_REPEAT) { - iter._has_next = false; - break; - } - } else { - iter._repetition_count = 0; - iter._last_word = buf; - } - out.append(buf, n); - - for (const auto &stop : _stop_sequences) { - size_t pos = out.find(stop); - if (pos != std::string::npos) { - // found stop sequence - truncate and signal end - out = out.substr(0, pos); - iter._has_next = false; - break; - } - } + // sample one token from the current logits + llama_token tok = llama_sampler_sample(_sampler, _ctx, -1); + + // end-of-generation check + if (llama_vocab_is_eog(_vocab, tok)) { + iter._has_next = false; + return ""; + } + + // decode the token + llama_batch batch = llama_batch_get_one(&tok, 1); + if (llama_decode(_ctx, batch)) { + _last_error = "Failed to evaluate token during generation"; + return ""; + } + + string out; + + if (!llama_vocab_is_control(_vocab, tok)) { + char buf[512]; + int n = llama_token_to_piece(_vocab, tok, buf, sizeof(buf), 0, false); + if (n > 0) { + if (iter._last_word == buf) { + if (++iter._repetition_count == MAX_REPEAT) { + iter._has_next = false; } + } else { + iter._repetition_count = 0; + iter._last_word = buf; } - } - } + out.append(buf, n); - // timing - auto t_end = std::chrono::high_resolution_clock::now(); - double secs = std::chrono::duration(t_end - t_start).count(); - iter._tokens_sec = secs > 0 ? generated / secs : 0; + if (++iter._tokens_generated > _max_tokens && ends_with_sentence_boundary(out)) { + iter._has_next = false; + } + for (const auto &stop : _stop_sequences) { + size_t pos = out.find(stop); + if (pos != std::string::npos) { + // found stop sequence - truncate and signal end + out = out.substr(0, pos); + iter._has_next = false; + break; + } + } + } + } return out; } diff --git a/llama/llama-sb.h b/llama/llama-sb.h index 6cc807d..e5299ee 100644 --- a/llama/llama-sb.h +++ b/llama/llama-sb.h @@ -7,6 +7,7 @@ #pragma once +#include #include #include #include "llama.h" @@ -20,9 +21,10 @@ struct LlamaIter { ~LlamaIter() {} Llama *_llama; - float _tokens_sec; string _last_word; + chrono::high_resolution_clock::time_point _t_start; int _repetition_count; + int _tokens_generated; bool _has_next; }; @@ -54,7 +56,9 @@ struct Llama { void reset(); private: + bool ends_with_sentence_boundary(const string &out); void configure_sampler(); + bool make_space_for_tokens(int n_tokens, int keep_min); vector tokenize(const string &prompt); llama_model *_model; diff --git a/llama/llama.cpp b/llama/llama.cpp index 380b4c9..af3be13 160000 --- a/llama/llama.cpp +++ b/llama/llama.cpp @@ -1 +1 @@ -Subproject commit 380b4c984e06f8d8381392d15814b3392e39560e +Subproject commit af3be131c065a38e476c34295bceda6cb956e7d7 diff --git a/llama/main.cpp b/llama/main.cpp index f93aa62..ee97f8e 100644 --- a/llama/main.cpp +++ b/llama/main.cpp @@ -276,8 +276,11 @@ static int cmd_llama_tokens_sec(var_s *self, int argc, slib_par_t *arg, var_s *r } else { int id = get_llama_iter_class_id(self, retval); if (id != -1) { - LlamaIter &llamaIter = g_llama_iter.at(id); - v_setreal(retval, llamaIter._tokens_sec); + LlamaIter &iter = g_llama_iter.at(id); + auto t_end = std::chrono::high_resolution_clock::now(); + double secs = std::chrono::duration(t_end - iter._t_start).count(); + double tokens_sec = secs > 0 ? iter._tokens_generated / secs : 0; + v_setreal(retval, tokens_sec); result = 1; } } diff --git a/llama/samples/adventure.bas b/llama/samples/adventure.bas new file mode 100644 index 0000000..eb88fea --- /dev/null +++ b/llama/samples/adventure.bas @@ -0,0 +1,117 @@ +import llm + +' Configuration +const n_ctx = 5000 +const n_batch = 512 +const model_path = "models/Qwen_Qwen2.5-1.5B-Instruct-GGUF-Q4/qwen2.5-1.5b-instruct-q4_k_m.gguf" +const max_turns = 10 + +' Initialize two separate LLM instances +const storyteller = llm.llama(model_path, n_ctx, n_batch) +const player = llm.llama(model_path, n_ctx, n_batch) + +' Configure Storyteller (creative, descriptive) +storyteller.set_max_tokens(150) +storyteller.set_temperature(0.8) +storyteller.set_top_k(80) +storyteller.set_top_p(0.95) +storyteller.set_min_p(0.05) +storyteller.set_penalty_repeat(1.2) +storyteller.set_penalty_last_n(128) + +' Stop sequences to prevent storyteller from acting as player +storyteller.add_stop("\nPlayer:") +storyteller.add_stop("\nYou:") +storyteller.add_stop("\nI ") +storyteller.add_stop("\n\n\n") + +' Configure Player (focused, action-oriented) +player.set_max_tokens(150) +player.set_temperature(0.9) +player.set_top_k(60) +player.set_top_p(0.9) +player.set_min_p(0.08) +player.set_penalty_repeat(1.15) +player.set_penalty_last_n(64) + +' Stop sequences to keep player from narrating outcomes +player.add_stop("\nStoryteller:") +player.add_stop("\nNarrator:") +player.add_stop("\nSuddenly") +player.add_stop("\nThe ") +player.add_stop("\n\n") + +' Game state +story_history = "" +turn_count = 0 + +' Initial storyteller prompt +storyteller_prompt = "You are a terse storyteller. IMPORTANT: IN ABSOLUTELY NO MORE THAN 3 sentances, " +storyteller_prompt += "begin an adventure story by describing the setting and initial situation for the hero. End with a situation that requires action.\n" +storyteller_prompt += "REMEMBER: keep it short !!!\n\nBegin the story:" + +print "=== AI ADVENTURE GAME ===" +print "Storyteller and Player will create a story together" +print "=========================" +print + +' Main game loop +while turn_count < max_turns + + ' === STORYTELLER TURN === + print "\e[32mSTORYTELLER (Turn " + str(turn_count + 1) + "):" + print "---" + + full_storyteller_prompt = storyteller_prompt + "\n\nPrevious story:\n" + story_history + + iter = storyteller.generate(full_storyteller_prompt) + storyteller_response = "" + while iter.has_next() + chunk = iter.next() + print chunk; + storyteller_response = storyteller_response + chunk + wend + print + print + + ' Update story history + story_history = story_history + "\nStoryteller: " + storyteller_response + + ' Check if story should end + if instr(lcase(storyteller_response), "the end") > 0 or instr(lcase(storyteller_response), "they lived") > 0 then + print "=== THE STORY CONCLUDES ===" + exit loop + fi + + ' === PLAYER TURN === + print "\e[33mPLAYER (Turn " + str(turn_count + 1) + "):" + print "---" + + player_prompt = "You are the hero in an adventure story. Based on the current situation, decide what action to take. Be decisive and specific. Respond in 1-2 sentences describing your action.\n\nCurrent situation:\n" + storyteller_response + "\n\nYour action:" + + iter = player.generate(player_prompt) + player_response = "" + while iter.has_next() + chunk = iter.next() + print chunk; + player_response = player_response + chunk + wend + print + print + + ' Update story history + story_history = story_history + "\nPlayer: " + player_response + + ' Update storyteller prompt for next turn + storyteller_prompt = "Continue the adventure story. The hero has taken an action. Describe what happens next as a result. Keep it to 2-3 sentences. Create tension, conflict, or new discoveries. End with a new situation requiring action.\n\nHero's last action: " + player_response + "\n\nWhat happens next:" + + turn_count = turn_count + 1 + + ' Optional: pause between turns + delay 500 +wend + +print +print "=========================" +print "Game completed after " + str(turn_count) + " turns" +print "=========================" diff --git a/llama/test_main.cpp b/llama/test_main.cpp index c24708f..62b9e9d 100644 --- a/llama/test_main.cpp +++ b/llama/test_main.cpp @@ -62,7 +62,7 @@ int main(int argc, char ** argv) { while (iter._has_next) { auto out = llama.next(iter); printf("\033[33m"); - printf(out.c_str()); + printf("%s\n", out.c_str()); printf("\n\033[0m"); } } else { From 066fa4cbcf38d65bceefaf57c59e3f1c8d2c675f Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Mon, 29 Dec 2025 18:35:08 +1030 Subject: [PATCH 076/131] LLM: plugin module - update build for GPU --- llama/CMakeLists.txt | 69 +++++++++++++++++++++-- llama/README.md | 84 ++++++++++++++++++++++++++++ llama/llama-sb.cpp | 68 +++++++++++++---------- llama/llama-sb.h | 2 +- llama/main.cpp | 5 +- llama/samples/chat.bas | 122 +++++++++++++++-------------------------- llama/samples/code.bas | 117 +++++++++++++++++++++++++++++++++++++++ llama/test_main.cpp | 2 +- 8 files changed, 353 insertions(+), 116 deletions(-) create mode 100644 llama/samples/code.bas diff --git a/llama/CMakeLists.txt b/llama/CMakeLists.txt index a7e54d1..768f127 100644 --- a/llama/CMakeLists.txt +++ b/llama/CMakeLists.txt @@ -11,8 +11,11 @@ set(CMAKE_C_STANDARD 11) # ----------------------------- set(LLAMA_DIR ${CMAKE_CURRENT_SOURCE_DIR}/llama.cpp) +set(LLAMA_BACKEND "AUTO" CACHE STRING "llama.cpp backend: AUTO, CPU, GPU, CUDA") +set_property(CACHE LLAMA_BACKEND PROPERTY STRINGS AUTO CPU GPU CUDA) + # ----------------------------- -# FORCE CPU-only static builds +# FORCE static builds # ----------------------------- # Disable all shared libraries globally set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) @@ -34,7 +37,21 @@ set(GGML_BUILD_SHARED OFF CACHE BOOL "" FORCE) set(GGML_BUILD_TESTS OFF CACHE BOOL "" FORCE) set(GGML_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) -# CPU-only flags +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +# ------------------------------- +# Define backend options +# ------------------------------- +set(LLAMA_BACKEND "AUTO" CACHE STRING "Select llama.cpp backend: AUTO, CPU, GPU, CUDA") +set_property(CACHE LLAMA_BACKEND PROPERTY STRINGS AUTO CPU GPU CUDA) + +# +# sudo apt install nvidia-open cuda-toolkit +# + +# ------------------------------- +# Disable all accelerators by default +# ------------------------------- set(GGML_OPENMP OFF CACHE BOOL "" FORCE) set(GGML_CUDA OFF CACHE BOOL "" FORCE) set(GGML_METAL OFF CACHE BOOL "" FORCE) @@ -42,8 +59,50 @@ set(GGML_OPENCL OFF CACHE BOOL "" FORCE) set(GGML_KOMPUTE OFF CACHE BOOL "" FORCE) set(GGML_SYCL OFF CACHE BOOL "" FORCE) set(GGML_ACCELERATE OFF CACHE BOOL "" FORCE) -set(GGML_NATIVE ON CACHE BOOL "" FORCE) -set(CMAKE_POSITION_INDEPENDENT_CODE ON) +set(GGML_NATIVE OFF CACHE BOOL "" FORCE) # default off + +# ------------------------------- +# Configure backends based on LLAMA_BACKEND +# ------------------------------- +include(CheckLanguage) + +if(LLAMA_BACKEND STREQUAL "CPU") + message(STATUS "llama.cpp backend: CPU-only") + set(GGML_NATIVE ON CACHE BOOL "" FORCE) # enable CPU SIMD optimizations + +elseif(LLAMA_BACKEND STREQUAL "GPU") + message(STATUS "llama.cpp backend: GPU (non-CUDA)") + set(GGML_OPENMP ON CACHE BOOL "" FORCE) # parallel CPU fallback + # GPU non-CUDA options can be added here in the future + +elseif(LLAMA_BACKEND STREQUAL "CUDA") + message(STATUS "llama.cpp backend: CUDA") + + check_language(CUDA) + if(CMAKE_CUDA_COMPILER) + enable_language(CUDA) + set(GGML_CUDA ON CACHE BOOL "" FORCE) + else() + message(FATAL_ERROR "CUDA backend requested but nvcc not found") + endif() + +elseif(LLAMA_BACKEND STREQUAL "AUTO") + message(STATUS "llama.cpp backend: AUTO") + + check_language(CUDA) + if(CMAKE_CUDA_COMPILER) + enable_language(CUDA) + set(GGML_CUDA ON CACHE BOOL "" FORCE) + message(STATUS "CUDA detected – enabling GGML_CUDA") + else() + set(GGML_OPENMP ON CACHE BOOL "" FORCE) + set(GGML_NATIVE ON CACHE BOOL "" FORCE) + message(STATUS "CUDA not found – using CPU/OpenMP") + endif() + +else() + message(FATAL_ERROR "Invalid LLAMA_BACKEND value: ${LLAMA_BACKEND}") +endif() # ----------------------------- # Add llama.cpp subdirectories @@ -129,7 +188,7 @@ if (ANDROID) ../include/hashmap.cpp ../include/apiexec.cpp ) - + # Optional: set the SONAME / versioning if you need it set_target_properties(llm_android PROPERTIES OUTPUT_NAME "libllm" diff --git a/llama/README.md b/llama/README.md index e6ed506..b4083a4 100644 --- a/llama/README.md +++ b/llama/README.md @@ -1,3 +1,87 @@ +1️⃣ Ensure nvidia-open driver is installed and working + +Check: + +`` +nvidia-smi +`` + +If it works, your driver is fine β€” no need to install the proprietary driver. + +2️⃣ Add NVIDIA CUDA repository + +``` +wget https://developer.download.nvidia.com/compute/cuda/repos/debian12/x86_64/cuda-keyring_1.1-1_all.deb +sudo dpkg -i cuda-keyring_1.1-1_all.deb +sudo apt update +``` + +This repo contains the latest CUDA toolkit for Debian 12. + +3️⃣ Install CUDA Toolkit only (no driver replacement) +sudo apt install -y cuda-toolkit + + +This installs: + +- nvcc compiler +- CUDA headers +- Runtime libraries (libcudart.so, etc.) + +4️⃣ Add CUDA to your environment + +``` +export PATH=/usr/local/cuda/bin:$PATH +export CUDAToolkit_ROOT=/usr/local/cuda +``` + +Optional: add to ~/.bashrc to make it permanent: + +``` +echo 'export PATH=/usr/local/cuda/bin:$PATH' >> ~/.bashrc +echo 'export CUDAToolkit_ROOT=/usr/local/cuda' >> ~/.bashrc +source ~/.bashrc +``` + +Verify: + +nvcc --version + +Should show something like: + +``` +nvcc: NVIDIA (R) Cuda compiler driver +Cuda compilation tools, release 12.4, V12.4.105 +``` + +5️⃣ Clean llama.cpp build directory + +``` +rm -rf build +mkdir build +cd build +``` + +6️⃣ Configure CMake for CUDA backend + +``` +cmake -DLLAMA_BACKEND=CUDA .. +``` + +You should now see: + +-- CUDA detected – enabling GGML_CUDA + +7️⃣ Build + +``` +make -j$(nproc) +``` + +The binary will use CUDA acceleration + +Note: fully static builds are not possible for CUDA; some .so libraries will remain dynamically linked (normal). + # Generator settings ## factual answers, tools, summaries diff --git a/llama/llama-sb.cpp b/llama/llama-sb.cpp index 0e5e57f..7ddec7b 100644 --- a/llama/llama-sb.cpp +++ b/llama/llama-sb.cpp @@ -65,11 +65,13 @@ void Llama::reset() { _max_tokens = 150; } -bool Llama::construct(string model_path, int n_ctx, int n_batch) { +bool Llama::construct(string model_path, int n_ctx, int n_batch, int n_gpu_layers) { ggml_backend_load_all(); llama_model_params mparams = llama_model_default_params(); - mparams.n_gpu_layers = 0; + if (n_gpu_layers >= 0) { + mparams.n_gpu_layers = n_gpu_layers; + } _model = llama_model_load_from_file(model_path.c_str(), mparams); if (!_model) { @@ -196,6 +198,8 @@ bool Llama::make_space_for_tokens(int n_tokens, int keep_min) { } bool Llama::generate(LlamaIter &iter, const string &prompt) { + configure_sampler(); + vector prompt_tokens = tokenize(prompt); if (prompt_tokens.size() == 0) { return false; @@ -233,8 +237,6 @@ bool Llama::generate(LlamaIter &iter, const string &prompt) { } } - configure_sampler(); - iter._t_start = std::chrono::high_resolution_clock::now(); iter._llama = this; iter._has_next = true; @@ -299,44 +301,50 @@ string Llama::next(LlamaIter &iter) { return ""; } - // decode the token - llama_batch batch = llama_batch_get_one(&tok, 1); - if (llama_decode(_ctx, batch)) { - _last_error = "Failed to evaluate token during generation"; - return ""; - } - - string out; - - if (!llama_vocab_is_control(_vocab, tok)) { - char buf[512]; - int n = llama_token_to_piece(_vocab, tok, buf, sizeof(buf), 0, false); - if (n > 0) { - if (iter._last_word == buf) { - if (++iter._repetition_count == MAX_REPEAT) { - iter._has_next = false; - } - } else { - iter._repetition_count = 0; - iter._last_word = buf; - } - out.append(buf, n); + string result; - if (++iter._tokens_generated > _max_tokens && ends_with_sentence_boundary(out)) { + //if (!llama_vocab_is_control(_vocab, tok)) { + char buf[512]; + int n = llama_token_to_piece(_vocab, tok, buf, sizeof(buf), 0, false); + if (n > 0) { + // detect repetition + if (iter._last_word == buf) { + if (++iter._repetition_count == MAX_REPEAT) { iter._has_next = false; } + } else { + iter._repetition_count = 0; + iter._last_word = buf; + } + + result.append(buf, n); + // detect end of max-tokens + if (++iter._tokens_generated > _max_tokens && ends_with_sentence_boundary(result)) { + iter._has_next = false; + } + + // detect stop words + if (iter._has_next) { for (const auto &stop : _stop_sequences) { - size_t pos = out.find(stop); + size_t pos = result.find(stop); if (pos != std::string::npos) { // found stop sequence - truncate and signal end - out = out.substr(0, pos); + result = result.substr(0, pos); iter._has_next = false; break; } } } } - return out; + + // prepare the next batch with the sampled token + llama_batch batch = llama_batch_get_one(&tok, 1); + if (llama_decode(_ctx, batch)) { + _last_error = "Failed to evaluate token during generation"; + return ""; + } + + return result; } diff --git a/llama/llama-sb.h b/llama/llama-sb.h index e5299ee..0811539 100644 --- a/llama/llama-sb.h +++ b/llama/llama-sb.h @@ -33,7 +33,7 @@ struct Llama { ~Llama(); // init - bool construct(string model_path, int n_ctx, int n_batch); + bool construct(string model_path, int n_ctx, int n_batch, int n_gpu_layers); // generation bool generate(LlamaIter &iter, const string &prompt); diff --git a/llama/main.cpp b/llama/main.cpp index ee97f8e..4670671 100644 --- a/llama/main.cpp +++ b/llama/main.cpp @@ -320,9 +320,10 @@ static int cmd_create_llama(int argc, slib_par_t *params, var_t *retval) { auto model = expand_path(get_param_str(argc, params, 0, "")); auto n_ctx = get_param_int(argc, params, 1, 2048); auto n_batch = get_param_int(argc, params, 2, 1024); + auto n_gpu_layers = get_param_int(argc, params, 3, -1); int id = ++g_nextId; Llama &llama = g_llama[id]; - if (llama.construct(model, n_ctx, n_batch)) { + if (llama.construct(model, n_ctx, n_batch, n_gpu_layers)) { map_init_id(retval, id, CLASS_ID_LLAMA); v_create_callback(retval, "add_stop", cmd_llama_add_stop); v_create_callback(retval, "generate", cmd_llama_generate); @@ -344,7 +345,7 @@ static int cmd_create_llama(int argc, slib_par_t *params, var_t *retval) { } FUNC_SIG lib_func[] = { - {1, 3, "LLAMA", cmd_create_llama}, + {1, 4, "LLAMA", cmd_create_llama}, }; SBLIB_API int sblib_func_count() { diff --git a/llama/samples/chat.bas b/llama/samples/chat.bas index 8fd68c9..0f6f697 100644 --- a/llama/samples/chat.bas +++ b/llama/samples/chat.bas @@ -1,84 +1,52 @@ import llm -const model = "models/Qwen_Qwen2.5-1.5B-Instruct-GGUF-Q4/qwen2.5-1.5b-instruct-q4_k_m.gguf" -const llama = llm.llama(model, 4096, 512) - -' llama.set_max_tokens(150) -' llama.set_min_p(0.5) -' llama.set_temperature(.8) -' llama.set_top_k(1) -' llama.set_top_p(0) - -rem factual answers, tools, summaries -' llama.set_max_tokens(150) -' llama.set_temperature(0.0) -' llama.set_top_k(1) -' llama.set_top_p(0.0) -' llama.set_min_p(0.0) - -rem assistant, Q+A, explanations, chat -' llama.set_max_tokens(150) -' llama.set_temperature(0.8) -' llama.set_top_k(40) -' llama.set_top_p(0.0) -' llama.set_min_p(0.05) +const n_ctx = 4000 +const n_batch = 8000 +const model_1 = "models/Qwen_Qwen2.5-3B-Instruct-GGUF-Q4/qwen2.5-3b-instruct-q4_k_m.gguf" +const llama = llm.llama(model_1, n_ctx, n_batch) rem creative, storytelling -' llama.set_max_tokens(200) -' llama.set_temperature(1.0) -' llama.set_top_k(80) -' llama.set_top_p(0.0) -' llama.set_min_p(0.1) - -rem surprises/loko -' llama.set_max_tokens(200) -' llama.set_temperature(1.2) -' llama.set_top_k(120) -' llama.set_top_p(0.0) -' llama.set_min_p(0.15) - -rem technical, conservative -' llama.set_max_tokens(150) -' llama.set_temperature(0.6) -' llama.set_top_k(30) -' llama.set_top_p(0.0) -' llama.set_min_p(0.02) - -rem speed optimised on CPU -llama.set_max_tokens(10) -llama.set_temperature(0.7) -llama.set_top_k(20) -llama.set_top_p(0.0) -llama.set_min_p(0.05) - -' // Conservative - minimal repetition control -' _penalty_last_n = 64; -' _penalty_repeat = 1.05f; - -' // Balanced - good default -' _penalty_last_n = 64; -' _penalty_repeat = 1.1f; - -' // Aggressive - strong anti-repetition -' _penalty_last_n = 128; -' _penalty_repeat = 1.2f; - -' // Disabled -' _penalty_last_n = 0; -' _penalty_repeat = 1.0f; - -' llama.set_penalty_repeat(1.15) -' llama.set_penalty_last_n(64) - -prompt = """\ -you are a helpful assistant\ - \nQuestion: when is dinner?\ -""" -print prompt -print "============" +llama.set_max_tokens(5500) +llama.set_temperature(.9) +llama.set_top_k(50) +llama.set_top_p(0.1) +llama.set_min_p(0.01) +llama.set_penalty_repeat(1.1) +llama.set_penalty_last_n(64) + +prompt = "You are an expert programmer in GPL SmallBASIC. Create a turn based game including fun ascii art\n" +prompt += "Here is an example of a type of class contruct in SmallBASIC which you may use\n" +prompt += "func Set\n" +prompt += " sub add(byref item)\n" +prompt += " local el \n" +prompt += " el.key = item.parent_key\n" +prompt += " el.map = item.map\n" +prompt += " self.items[item.key] = el\n" +prompt += " end\n" +prompt += " func contains(byref item)\n" +prompt += " return self.items[item.key].key != 0\n" +prompt += " end\n" +prompt += " func get(byref key)\n" +prompt += " return self.items[key]\n" +prompt += " end\n" +prompt += " local result = {}\n" +prompt += " result.items = {}\n" +prompt += " result.add = @add\n" +prompt += " result.contains= @contains\n" +prompt += " result.get = @get\n" +prompt += " return result\n" +prompt += "end\n" +prompt += "IMPORTANT: this dialect does not support the class keyword. Do not use it !!!\n" +prompt += "Always end a FUNC with the RETURN statement. For routines the do not return a value, use SUB\n" +prompt += "Use the LOCAL keyword inside a SUB or FUNC to declare local variables\n" +prompt += "Never declare FUNCTION, use FUNC instead\n" + +print prompt; iter = llama.generate(prompt) while iter.has_next() - print iter.next() + print iter.next(); wend -print "============" -print iter.tokens_sec() + +print +print "===========" +print "tokens/sec "; iter.tokens_sec() diff --git a/llama/samples/code.bas b/llama/samples/code.bas new file mode 100644 index 0000000..6762fbb --- /dev/null +++ b/llama/samples/code.bas @@ -0,0 +1,117 @@ +import llm + +const n_ctx = 2048 +const n_batch = 2048 +const model = "models/Qwen_Qwen2.5-3B-Instruct-GGUF-Q4/qwen2.5-3b-instruct-q4_k_m.gguf" +const llama = llm.llama(model, n_ctx, n_batch) + +rem creative, storytelling +llama.set_max_tokens(5000) +llama.set_temperature(0.8) +llama.set_top_k(60) +llama.set_top_p(0.0) +llama.set_min_p(0.02) +llama.set_penalty_repeat(1.1) +llama.set_penalty_last_n(64) + +code = "" +code += "func hsv_to_rgb(hsv)" +code += " [h,s,v] = hsv" +code += "" +code += " c = v * s" +code += " x = c * (1 - abs((h / 60) % 2 - 1))" +code += " m = v - c" +code += "" +code += " if 0 <= h < 60 then" +code += " [r1, g1, b1] = [c, x, 0]" +code += " else if h >= 60 && h < 120 then" +code += " [r1, g1, b1] = [x, c, 0]" +code += " else if h >= 120 && h < 180 then" +code += " [r1, g1, b1] = [0, c, x]" +code += " else if h >= 180 && h < 240 then" +code += " [r1, g1, b1] = [0, x, c]" +code += " else if h >= 240 && h < 300 then" +code += " [r1, g1, b1] = [x, 0, c]" +code += " else" +code += " [r1, g1, b1] = [c, 0, x]" +code += " endif " +code += "" +code += " # Add m to match value" +code += " r = (r1 + m) * 255" +code += " g = (g1 + m) * 255" +code += " b = (b1 + m) * 255" +code += " return \"#\" + hex(round(r)) + hex(round(g)) + hex(round(b))" +code += "end " +code += "" +code += "func rgb_to_hsv(_rgb)" +code += " [r,g,b] = _rgb" +code += "" +code += " r = r / 255.0" +code += " g = g / 255.0" +code += " b = b / 255.0" +code += " local max_val = max(r, g, b)" +code += " local min_val = min(r, g, b)" +code += " local delta = max_val - min_val" +code += " " +code += " # Hue calculation" +code += " if delta == 0 then " +code += " h = 0" +code += " else if max_val == r then" +code += " h = 60 * (((g - b) / delta) % 6)" +code += " else if max_val == g then" +code += " h = 60 * (((b - r) / delta) + 2)" +code += " else" +code += " # max_val == b" +code += " h = 60 * (((r - g) / delta) + 4)" +code += " endif" +code += " # Saturation calculation" +code += " if max_val == 0 then" +code += " s = 0" +code += " else" +code += " s = delta / max_val" +code += " # Value calculation" +code += " v = max_val" +code += " endif " +code += " return [h, s, v]" +code += "end" +code += "" +code += "func generate_distinct_colors(initial_rgb, n)" +code += " # Convert the initial RGB color to HSV" +code += " # Extract hue, saturation, value" +code += " [hue, saturation, value] = rgb_to_hsv(initial_rgb)" +code += "" +code += " # Create an empty list to store colors" +code += " dim color_list" +code += " color_list << initial_rgb" +code += "" +code += " # Compute hue increment to spread colors evenly around the color wheel" +code += " local hue_increment = 360 / n" +code += "" +code += " for i = 1 to n-1" +code += " # Generate next hue" +code += " new_hue = (hue + i * hue_increment) % 360" +code += " " +code += " # Keep saturation and value the same for brightness/contrast consistency" +code += " new_hsv = [new_hue, saturation, value]" +code += " " +code += " # Convert back to RGB" +code += " new_rgb = hsv_to_rgb(new_hsv)" +code += " " +code += " # Add new RGB color to list" +code += " color_list << new_rgb" +code += " next " +code += " return color_list" +code += "end " +code += "" +code += "colors = generate_distinct_colors([06, 40, 54], 10)" +code += "print colors" + +prompt = "You are a helpful coding assistant.\nUpdate this BASIC (SmallBASIC) program to use the golden-ratio to create individual colors.\n" +prompt += "\nIndent with two spaces and don't give long lines\n" +prompt += "\nThe golden-ratio 1.6180339887\n" +prompt += code + +iter = llama.generate(prompt) +while iter.has_next() + print iter.next(); +wend diff --git a/llama/test_main.cpp b/llama/test_main.cpp index 62b9e9d..5291b9d 100644 --- a/llama/test_main.cpp +++ b/llama/test_main.cpp @@ -56,7 +56,7 @@ int main(int argc, char ** argv) { } Llama llama; - if (llama.construct(model_path, 1024, 1024)) { + if (llama.construct(model_path, 1024, 1024, -1)) { LlamaIter iter; llama.generate(iter, prompt); while (iter._has_next) { From 9f10e732402ef2d5bee2cc3ec92e96e7e844a4cb Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Wed, 31 Dec 2025 11:12:48 +1030 Subject: [PATCH 077/131] LLM: plugin module - added all() method to generate iterator --- llama/README.md | 12 ++++++++ llama/llama-sb.cpp | 76 +++++++++++++++++++++++++++++++++++----------- llama/llama-sb.h | 2 ++ llama/main.cpp | 20 ++++++++++++ 4 files changed, 93 insertions(+), 17 deletions(-) diff --git a/llama/README.md b/llama/README.md index b4083a4..e1435ba 100644 --- a/llama/README.md +++ b/llama/README.md @@ -1,3 +1,15 @@ +## huggingface-cli + +``` +pyenv virtualenv 3.10.13 hf-tools +pyenv activate hf-tools +pip install -U pip +pip install huggingface_hub + +``` + +--- + 1️⃣ Ensure nvidia-open driver is installed and working Check: diff --git a/llama/llama-sb.cpp b/llama/llama-sb.cpp index 7ddec7b..7ee3ec5 100644 --- a/llama/llama-sb.cpp +++ b/llama/llama-sb.cpp @@ -286,24 +286,8 @@ bool Llama::ends_with_sentence_boundary(const string &text) { return false; } -string Llama::next(LlamaIter &iter) { - if (!iter._has_next) { - _last_error = "Iteration beyond end of stream"; - return ""; - } - - // sample one token from the current logits - llama_token tok = llama_sampler_sample(_sampler, _ctx, -1); - - // end-of-generation check - if (llama_vocab_is_eog(_vocab, tok)) { - iter._has_next = false; - return ""; - } - +string Llama::token_to_string(LlamaIter &iter, llama_token tok) { string result; - - //if (!llama_vocab_is_control(_vocab, tok)) { char buf[512]; int n = llama_token_to_piece(_vocab, tok, buf, sizeof(buf), 0, false); if (n > 0) { @@ -337,6 +321,25 @@ string Llama::next(LlamaIter &iter) { } } } + return result; +} + +string Llama::next(LlamaIter &iter) { + if (!iter._has_next) { + _last_error = "Iteration beyond end of stream"; + return ""; + } + + // sample the next token from the current logits + llama_token tok = llama_sampler_sample(_sampler, _ctx, -1); + + // end-of-generation check + if (llama_vocab_is_eog(_vocab, tok)) { + iter._has_next = false; + return ""; + } + + string result = token_to_string(iter, tok); // prepare the next batch with the sampled token llama_batch batch = llama_batch_get_one(&tok, 1); @@ -348,3 +351,42 @@ string Llama::next(LlamaIter &iter) { return result; } +string Llama::all(LlamaIter &iter) { + string out; + + vector decoded; + decoded.reserve(_max_tokens); + + int generated = 0; + + while (generated < _max_tokens) { + // sample the next token from the current logits + llama_token tok = llama_sampler_sample(_sampler, _ctx, -1); + + // end-of-generation check + if (llama_vocab_is_eog(_vocab, tok)) { + iter._has_next = false; + break; + } + + // append token to decoded list + decoded.push_back(tok); + ++generated; + + // decode the token + llama_batch batch = llama_batch_get_one(&tok, 1); + if (llama_decode(_ctx, batch)) { + _last_error = "Failed to evaluate token during generation"; + break; + } + } + + // detokenize sequentially + if (!decoded.empty()) { + for (llama_token tok : decoded) { + out.append(token_to_string(iter, tok)); + } + } + + return out; +} diff --git a/llama/llama-sb.h b/llama/llama-sb.h index 0811539..b1da148 100644 --- a/llama/llama-sb.h +++ b/llama/llama-sb.h @@ -38,6 +38,7 @@ struct Llama { // generation bool generate(LlamaIter &iter, const string &prompt); string next(LlamaIter &iter); + string all(LlamaIter &iter); // generation parameters void add_stop(const char *stop) { _stop_sequences.push_back(stop); } @@ -60,6 +61,7 @@ struct Llama { void configure_sampler(); bool make_space_for_tokens(int n_tokens, int keep_min); vector tokenize(const string &prompt); + string token_to_string(LlamaIter &iter, llama_token tok); llama_model *_model; llama_context *_ctx; diff --git a/llama/main.cpp b/llama/main.cpp index 4670671..1c0de21 100644 --- a/llama/main.cpp +++ b/llama/main.cpp @@ -229,6 +229,25 @@ static int cmd_llama_reset(var_s *self, int argc, slib_par_t *arg, var_s *retval return result; } +// +// iter.all() +// +static int cmd_llama_all(var_s *self, int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + if (argc != 0) { + error(retval, "iter.all", 0, 0); + } else { + int id = get_llama_iter_class_id(self, retval); + if (id != -1) { + LlamaIter &iter = g_llama_iter.at(id); + auto out = iter._llama->all(iter); + v_setstr(retval, out.c_str()); + result = 1; + } + } + return result; +} + // // iter.has_next() // @@ -303,6 +322,7 @@ static int cmd_llama_generate(var_s *self, int argc, slib_par_t *arg, var_s *ret auto prompt = get_param_str(argc, arg, 0, ""); if (llama.generate(iter, prompt)) { map_init_id(retval, iter_id, CLASS_ID_LLAMA_ITER); + v_create_callback(retval, "all", cmd_llama_all); v_create_callback(retval, "has_next", cmd_llama_has_next); v_create_callback(retval, "next", cmd_llama_next); v_create_callback(retval, "tokens_sec", cmd_llama_tokens_sec); From 5eabd3d0ba1451c2c863532e80d18009656adeff Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Fri, 2 Jan 2026 07:16:07 +1030 Subject: [PATCH 078/131] Cleanup build file --- llama/CMakeLists.txt | 15 ++++++--------- llama/llama-sb.cpp | 3 +++ llama/test_main.cpp | 1 + 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/llama/CMakeLists.txt b/llama/CMakeLists.txt index 768f127..56ee204 100644 --- a/llama/CMakeLists.txt +++ b/llama/CMakeLists.txt @@ -11,9 +11,6 @@ set(CMAKE_C_STANDARD 11) # ----------------------------- set(LLAMA_DIR ${CMAKE_CURRENT_SOURCE_DIR}/llama.cpp) -set(LLAMA_BACKEND "AUTO" CACHE STRING "llama.cpp backend: AUTO, CPU, GPU, CUDA") -set_property(CACHE LLAMA_BACKEND PROPERTY STRINGS AUTO CPU GPU CUDA) - # ----------------------------- # FORCE static builds # ----------------------------- @@ -54,12 +51,12 @@ set_property(CACHE LLAMA_BACKEND PROPERTY STRINGS AUTO CPU GPU CUDA) # ------------------------------- set(GGML_OPENMP OFF CACHE BOOL "" FORCE) set(GGML_CUDA OFF CACHE BOOL "" FORCE) -set(GGML_METAL OFF CACHE BOOL "" FORCE) -set(GGML_OPENCL OFF CACHE BOOL "" FORCE) -set(GGML_KOMPUTE OFF CACHE BOOL "" FORCE) -set(GGML_SYCL OFF CACHE BOOL "" FORCE) -set(GGML_ACCELERATE OFF CACHE BOOL "" FORCE) -set(GGML_NATIVE OFF CACHE BOOL "" FORCE) # default off +set(GGML_NATIVE OFF CACHE BOOL "" FORCE) +set(GGML_METAL OFF CACHE BOOL "" FORCE) # Apple GPU API +set(GGML_OPENCL OFF CACHE BOOL "" FORCE) # Cross platform GPU API (AMD/Intel) +set(GGML_KOMPUTE OFF CACHE BOOL "" FORCE) # Vulcan/more modern than OpenCL +set(GGML_SYCL OFF CACHE BOOL "" FORCE) # Intel +set(GGML_ACCELERATE OFF CACHE BOOL "" FORCE) # Apple # ------------------------------- # Configure backends based on LLAMA_BACKEND diff --git a/llama/llama-sb.cpp b/llama/llama-sb.cpp index 7ee3ec5..2bff5e8 100644 --- a/llama/llama-sb.cpp +++ b/llama/llama-sb.cpp @@ -63,6 +63,9 @@ void Llama::reset() { _top_p = 1.0f; _min_p = 0.0f; _max_tokens = 150; + if (_ctx) { + llama_memory_clear(llama_get_memory(_ctx), true); + } } bool Llama::construct(string model_path, int n_ctx, int n_batch, int n_gpu_layers) { diff --git a/llama/test_main.cpp b/llama/test_main.cpp index 5291b9d..2c082bf 100644 --- a/llama/test_main.cpp +++ b/llama/test_main.cpp @@ -58,6 +58,7 @@ int main(int argc, char ** argv) { Llama llama; if (llama.construct(model_path, 1024, 1024, -1)) { LlamaIter iter; + llama.set_max_tokens(n_predict); llama.generate(iter, prompt); while (iter._has_next) { auto out = llama.next(iter); From e8500cfe9582b61b5c2cbe43af7a26361b0b2ae7 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Fri, 2 Jan 2026 08:25:40 +1030 Subject: [PATCH 079/131] RAYLIB: build with SDL3 platform backend --- configure.ac | 11 ++++++++++- raylib/Makefile.am | 8 +++----- raylib/main.cpp | 18 +++++++----------- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/configure.ac b/configure.ac index f3a23f0..ed3c8a5 100644 --- a/configure.ac +++ b/configure.ac @@ -48,6 +48,13 @@ AC_ARG_WITH(ioio, [IOIO="yes"], [IOIO="no"]) +dnl configure SDL3 for raylib +PKG_CHECK_MODULES([SDL3], [sdl3]) +AC_MSG_CHECKING([for SDL3 library linking]) +SDL3_LIBS=`pkg-config sdl3 --libs --static` +SDL3_CFLAGS=`pkg-config sdl3 --cflags` +SDL3_INCLUDE=-I`pkg-config --variable=includedir sdl3`/SDL3 + case "${host_os}" in *mingw* | *msys*) AC_DEFINE(_WIN32, 1, [building for win32]) @@ -70,7 +77,8 @@ case "${host_os}" in WEBSOCKET_LDFLAGS="" GTK_SERVER_LDFLAGS="`pkg-config --libs gtk+-3.0` -lXm -lXt" GTK_SERVER_CPPFLAGS="`pkg-config --cflags gtk+-3.0` -DGTK_SERVER_FFI -DGTK_SERVER_LIBRARY -DGTK_SERVER_UNIX -DGTK_SERVER_GTK3x" - RAYLIB_LDFLAGS="-lGL -lm -lpthread -ldl -lrt -lX11 -lwayland-client -lwayland-cursor -lwayland-egl -lxkbcommon" + RAYLIB_CPPFLAGS="${SDL3_INCLUDE} ${SDL3_CFLAGS}" + RAYLIB_LDFLAGS="${SDL3_LIBS}" JVM_CPPFLAGS="-I/usr/lib/jvm/java-1.8.0-openjdk-amd64/include -I/usr/lib/jvm/java-1.8.0-openjdk-amd64/include/linux" JVM_LDFLAGS="-L/usr/lib/jvm/java-1.8.0-openjdk-amd64/jre/lib/amd64/server -ljvm" NUKLEAR_CPPFLAGS="-D_GLFW_X11=1" @@ -79,6 +87,7 @@ esac AC_SUBST(DEBUG_LDFLAGS) AC_SUBST(CLIPBOARD_LDFLAGS) AC_SUBST(RAYLIB_LDFLAGS) +AC_SUBST(RAYLIB_CPPFLAGS) AC_SUBST(NUKLEAR_LDFLAGS) AC_SUBST(WEBSOCKET_LDFLAGS) AC_SUBST(PLATFORM_LDFLAGS) diff --git a/raylib/Makefile.am b/raylib/Makefile.am index 875d5dc..ab155c1 100644 --- a/raylib/Makefile.am +++ b/raylib/Makefile.am @@ -24,16 +24,14 @@ $(generated): raylib/parser/raylib_api.json mkraylib.bas gen: $(generated) README.md AM_CXXFLAGS=-fno-rtti -std=c++14 -fpermissive -AM_CPPFLAGS = -Iraylib/src -Iraylib/src/external/glfw/include -Iraylib/src/external/glfw/deps/mingw \ - -Iraylib/src/external/glfw/src \ - -DPLATFORM_DESKTOP=1 -DSUPPORT_BUSY_WAIT_LOOP=1 -DSUPPORT_SCREEN_CAPTURE=1 \ - -DSUPPORT_GIF_RECORDING=1 -DSUPPORT_COMPRESSION_API=1 -D_GLFW_WAYLAND=1 \ +AM_CPPFLAGS = -Iraylib/src @RAYLIB_CPPFLAGS@ \ + -DPLATFORM_DESKTOP_SDL=1 -DPLATFORM_DESKTOP_SDL3=1 -DSUPPORT_BUSY_WAIT_LOOP=1 -DSUPPORT_SCREEN_CAPTURE=1 \ + -DSUPPORT_GIF_RECORDING=1 -DSUPPORT_COMPRESSION_API=1 \ -Wall -Wextra -Wshadow -Wdouble-promotion -Wno-unused-parameter -fPIC lib_LTLIBRARIES = libraylib.la libraylib_la_SOURCES = \ - raylib/src/rglfw.c \ raylib/src/rmodels.c \ raylib/src/raudio.c \ raylib/src/rcore.c \ diff --git a/raylib/main.cpp b/raylib/main.cpp index 8bed447..4d8a576 100644 --- a/raylib/main.cpp +++ b/raylib/main.cpp @@ -24,10 +24,10 @@ #pragma GCC diagnostic pop #pragma GCC diagnostic pop #include -#include #include #include "robin-hood-hashing/src/include/robin_hood.h" +#include "SDL_events.h" #include "include/var.h" #include "include/module.h" #include "include/param.h" @@ -1377,18 +1377,15 @@ static int cmd_guiunlock(int argc, slib_par_t *params, var_t *retval) { return 1; } -static int cmd_poll_events(int argc, slib_par_t *params, var_t *retval) { - glfwPollEvents(); - return 1; -} - static int cmd_wait_events(int argc, slib_par_t *params, var_t *retval) { - float waitMillis = get_param_int(argc, params, 0, -1); - if (waitMillis > 0) { - glfwWaitEventsTimeout(waitMillis / 1000); + auto timeoutMS = get_param_int(argc, params, 0, -1); + SDL_Event event; + if (timeoutMS > 0) { + SDL_WaitEventTimeout(&event, timeoutMS); } else { - glfwWaitEvents(); + SDL_WaitEvent(&event); } + SDL_PushEvent(&event); return 1; } @@ -1835,7 +1832,6 @@ static FUNC_SIG lib_proc[] = { {3, 3, "GUISETSTYLE", cmd_guisetstyle}, {2, 2, "GUISTATUSBAR", cmd_guistatusbar}, {0, 0, "GUIUNLOCK", cmd_guiunlock}, - {0, 0, "POLLEVENTS", cmd_poll_events}, {0, 1, "WAITEVENTS", cmd_wait_events}, {0, 0, "CLOSEPHYSICS", cmd_closephysics}, {1, 1, "DESTROYPHYSICSBODY", cmd_destroyphysicsbody}, From 4a59ea1621021ef6ee6748f8ecb2ca54df64ef82 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Fri, 2 Jan 2026 13:00:55 +1030 Subject: [PATCH 080/131] RAYLIB: add missing include for flatpak build --- raylib/main.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/raylib/main.cpp b/raylib/main.cpp index 4d8a576..ebc1759 100644 --- a/raylib/main.cpp +++ b/raylib/main.cpp @@ -25,6 +25,7 @@ #pragma GCC diagnostic pop #include #include +#include #include "robin-hood-hashing/src/include/robin_hood.h" #include "SDL_events.h" From c5ace4e9ed22c459ea259df6036a021a0e823440 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Mon, 5 Jan 2026 09:11:25 +1030 Subject: [PATCH 081/131] Revert "RAYLIB: build with SDL3 platform backend" This reverts commit e8500cfe9582b61b5c2cbe43af7a26361b0b2ae7. Switching to SDL from libgl doesn't completely solve running from the IDE --- configure.ac | 11 +---------- raylib/Makefile.am | 8 +++++--- raylib/main.cpp | 18 +++++++++++------- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/configure.ac b/configure.ac index ed3c8a5..f3a23f0 100644 --- a/configure.ac +++ b/configure.ac @@ -48,13 +48,6 @@ AC_ARG_WITH(ioio, [IOIO="yes"], [IOIO="no"]) -dnl configure SDL3 for raylib -PKG_CHECK_MODULES([SDL3], [sdl3]) -AC_MSG_CHECKING([for SDL3 library linking]) -SDL3_LIBS=`pkg-config sdl3 --libs --static` -SDL3_CFLAGS=`pkg-config sdl3 --cflags` -SDL3_INCLUDE=-I`pkg-config --variable=includedir sdl3`/SDL3 - case "${host_os}" in *mingw* | *msys*) AC_DEFINE(_WIN32, 1, [building for win32]) @@ -77,8 +70,7 @@ case "${host_os}" in WEBSOCKET_LDFLAGS="" GTK_SERVER_LDFLAGS="`pkg-config --libs gtk+-3.0` -lXm -lXt" GTK_SERVER_CPPFLAGS="`pkg-config --cflags gtk+-3.0` -DGTK_SERVER_FFI -DGTK_SERVER_LIBRARY -DGTK_SERVER_UNIX -DGTK_SERVER_GTK3x" - RAYLIB_CPPFLAGS="${SDL3_INCLUDE} ${SDL3_CFLAGS}" - RAYLIB_LDFLAGS="${SDL3_LIBS}" + RAYLIB_LDFLAGS="-lGL -lm -lpthread -ldl -lrt -lX11 -lwayland-client -lwayland-cursor -lwayland-egl -lxkbcommon" JVM_CPPFLAGS="-I/usr/lib/jvm/java-1.8.0-openjdk-amd64/include -I/usr/lib/jvm/java-1.8.0-openjdk-amd64/include/linux" JVM_LDFLAGS="-L/usr/lib/jvm/java-1.8.0-openjdk-amd64/jre/lib/amd64/server -ljvm" NUKLEAR_CPPFLAGS="-D_GLFW_X11=1" @@ -87,7 +79,6 @@ esac AC_SUBST(DEBUG_LDFLAGS) AC_SUBST(CLIPBOARD_LDFLAGS) AC_SUBST(RAYLIB_LDFLAGS) -AC_SUBST(RAYLIB_CPPFLAGS) AC_SUBST(NUKLEAR_LDFLAGS) AC_SUBST(WEBSOCKET_LDFLAGS) AC_SUBST(PLATFORM_LDFLAGS) diff --git a/raylib/Makefile.am b/raylib/Makefile.am index ab155c1..875d5dc 100644 --- a/raylib/Makefile.am +++ b/raylib/Makefile.am @@ -24,14 +24,16 @@ $(generated): raylib/parser/raylib_api.json mkraylib.bas gen: $(generated) README.md AM_CXXFLAGS=-fno-rtti -std=c++14 -fpermissive -AM_CPPFLAGS = -Iraylib/src @RAYLIB_CPPFLAGS@ \ - -DPLATFORM_DESKTOP_SDL=1 -DPLATFORM_DESKTOP_SDL3=1 -DSUPPORT_BUSY_WAIT_LOOP=1 -DSUPPORT_SCREEN_CAPTURE=1 \ - -DSUPPORT_GIF_RECORDING=1 -DSUPPORT_COMPRESSION_API=1 \ +AM_CPPFLAGS = -Iraylib/src -Iraylib/src/external/glfw/include -Iraylib/src/external/glfw/deps/mingw \ + -Iraylib/src/external/glfw/src \ + -DPLATFORM_DESKTOP=1 -DSUPPORT_BUSY_WAIT_LOOP=1 -DSUPPORT_SCREEN_CAPTURE=1 \ + -DSUPPORT_GIF_RECORDING=1 -DSUPPORT_COMPRESSION_API=1 -D_GLFW_WAYLAND=1 \ -Wall -Wextra -Wshadow -Wdouble-promotion -Wno-unused-parameter -fPIC lib_LTLIBRARIES = libraylib.la libraylib_la_SOURCES = \ + raylib/src/rglfw.c \ raylib/src/rmodels.c \ raylib/src/raudio.c \ raylib/src/rcore.c \ diff --git a/raylib/main.cpp b/raylib/main.cpp index ebc1759..667f0dc 100644 --- a/raylib/main.cpp +++ b/raylib/main.cpp @@ -24,11 +24,11 @@ #pragma GCC diagnostic pop #pragma GCC diagnostic pop #include +#include #include #include #include "robin-hood-hashing/src/include/robin_hood.h" -#include "SDL_events.h" #include "include/var.h" #include "include/module.h" #include "include/param.h" @@ -1378,15 +1378,18 @@ static int cmd_guiunlock(int argc, slib_par_t *params, var_t *retval) { return 1; } +static int cmd_poll_events(int argc, slib_par_t *params, var_t *retval) { + glfwPollEvents(); + return 1; +} + static int cmd_wait_events(int argc, slib_par_t *params, var_t *retval) { - auto timeoutMS = get_param_int(argc, params, 0, -1); - SDL_Event event; - if (timeoutMS > 0) { - SDL_WaitEventTimeout(&event, timeoutMS); + float waitMillis = get_param_int(argc, params, 0, -1); + if (waitMillis > 0) { + glfwWaitEventsTimeout(waitMillis / 1000); } else { - SDL_WaitEvent(&event); + glfwWaitEvents(); } - SDL_PushEvent(&event); return 1; } @@ -1833,6 +1836,7 @@ static FUNC_SIG lib_proc[] = { {3, 3, "GUISETSTYLE", cmd_guisetstyle}, {2, 2, "GUISTATUSBAR", cmd_guistatusbar}, {0, 0, "GUIUNLOCK", cmd_guiunlock}, + {0, 0, "POLLEVENTS", cmd_poll_events}, {0, 1, "WAITEVENTS", cmd_wait_events}, {0, 0, "CLOSEPHYSICS", cmd_closephysics}, {1, 1, "DESTROYPHYSICSBODY", cmd_destroyphysicsbody}, From d9f64b1d968cc2f380109fcbe070eeed7c3d6fee Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Mon, 5 Jan 2026 20:06:52 +1030 Subject: [PATCH 082/131] RAYLIB: programs must run in a separate thread or via the command line --- include/module.h | 11 ++++++++++- raylib/main.cpp | 8 +++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/include/module.h b/include/module.h index d27ea52..b9176aa 100644 --- a/include/module.h +++ b/include/module.h @@ -25,6 +25,15 @@ extern "C" { */ int sblib_init(const char *sourceFile); +/** + * @ingroup modstd + * + * Returns whether the module is compatible with IDE builds + * + * @return non-zero on success + */ +int sblib_is_ide_compatible(void); + /** * @ingroup modstd * @@ -116,7 +125,7 @@ int sblib_func_exec(int index, int param_count, slib_par_t *params, var_t *retva * @param cls_id the variable class identifier * @param id the variable instance identifier */ -void sblib_free(int cls_id, int id); +int sblib_free(int cls_id, int id); /** * @ingroup modlib diff --git a/raylib/main.cpp b/raylib/main.cpp index 667f0dc..98ad484 100644 --- a/raylib/main.cpp +++ b/raylib/main.cpp @@ -1938,7 +1938,7 @@ SBLIB_API int sblib_func_exec(int index, int argc, slib_par_t *params, var_t *re return result; } -SBLIB_API void sblib_free(int cls_id, int id) { +SBLIB_API int sblib_free(int cls_id, int id) { if (id != -1) { switch (cls_id) { case CLS_AUDIOSTREAM: @@ -2031,6 +2031,7 @@ SBLIB_API void sblib_free(int cls_id, int id) { break; } } + return 0; } SBLIB_API void sblib_close(void) { @@ -2095,3 +2096,8 @@ SBLIB_API void sblib_close(void) { _waveMap.clear(); } } + +SBLIB_API void sblib_is_ide_compatible(void) { + // when using the SQL build, programs must be run via a separate thread + return false; +} From 8c4a66facc1ed4bb306be16d67d62ae617bce3cc Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Mon, 5 Jan 2026 20:09:23 +1030 Subject: [PATCH 083/131] RAYLIB: programs must run in a separate thread or via the command line --- raylib/main.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/raylib/main.cpp b/raylib/main.cpp index 98ad484..8238cb4 100644 --- a/raylib/main.cpp +++ b/raylib/main.cpp @@ -2097,7 +2097,7 @@ SBLIB_API void sblib_close(void) { } } -SBLIB_API void sblib_is_ide_compatible(void) { +SBLIB_API int sblib_is_ide_compatible(void) { // when using the SQL build, programs must be run via a separate thread - return false; + return 0; } From f615e24344adecb842741afdfc2d8ce85844c16d Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Tue, 6 Jan 2026 21:22:39 +1030 Subject: [PATCH 084/131] RAYLIB: rename UI detection to sblib_has_window_ui() --- include/module.h | 2 +- raylib/main.cpp | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/include/module.h b/include/module.h index b9176aa..c7753b2 100644 --- a/include/module.h +++ b/include/module.h @@ -32,7 +32,7 @@ int sblib_init(const char *sourceFile); * * @return non-zero on success */ -int sblib_is_ide_compatible(void); +int sblib_has_window_ui(void); /** * @ingroup modstd diff --git a/raylib/main.cpp b/raylib/main.cpp index 8238cb4..18ee3cd 100644 --- a/raylib/main.cpp +++ b/raylib/main.cpp @@ -2097,7 +2097,7 @@ SBLIB_API void sblib_close(void) { } } -SBLIB_API int sblib_is_ide_compatible(void) { - // when using the SQL build, programs must be run via a separate thread - return 0; +SBLIB_API int sblib_has_window_ui(void) { + // raylib module creates a UI in a new window + return 1; } From 744b20036d98f67dcba9f9998df888fa99f546c7 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Thu, 15 Jan 2026 08:50:34 +1030 Subject: [PATCH 085/131] FLATPAK: implements sblib_has_window_ui() in UI based modules --- glfw/main.cpp | 4 ++++ nuklear/main.cpp | 4 ++++ raylib/main.cpp | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/glfw/main.cpp b/glfw/main.cpp index 2a49198..07f8872 100644 --- a/glfw/main.cpp +++ b/glfw/main.cpp @@ -404,3 +404,7 @@ SBLIB_API void sblib_ellipse(int xc, int yc, int xr, int yr, int fill) { glEnd(); } +SBLIB_API int sblib_has_window_ui(void) { + // module creates a UI in a new window + return 1; +} diff --git a/nuklear/main.cpp b/nuklear/main.cpp index 00ac348..9553b7c 100644 --- a/nuklear/main.cpp +++ b/nuklear/main.cpp @@ -1181,3 +1181,7 @@ SBLIB_API void sblib_ellipse(int xc, int yc, int xr, int yr, int fill) { drawEnd(); } +SBLIB_API int sblib_has_window_ui(void) { + // module creates a UI in a new window + return 1; +} diff --git a/raylib/main.cpp b/raylib/main.cpp index 18ee3cd..06598c9 100644 --- a/raylib/main.cpp +++ b/raylib/main.cpp @@ -2098,6 +2098,6 @@ SBLIB_API void sblib_close(void) { } SBLIB_API int sblib_has_window_ui(void) { - // raylib module creates a UI in a new window + // module creates a UI in a new window return 1; } From 5cf27c37cc948728ac11b15abdbe21b687f661c5 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Sun, 18 Jan 2026 15:18:09 +1030 Subject: [PATCH 086/131] FLATPAK: ui modules now target wayland in linux build --- configure.ac | 35 ++++++++++++++++++++++++++++++++--- glfw/Makefile.am | 1 + nuklear/Makefile.am | 1 + nuklear/main.cpp | 26 ++++++++++++++++---------- 4 files changed, 50 insertions(+), 13 deletions(-) diff --git a/configure.ac b/configure.ac index f3a23f0..e3f5f79 100644 --- a/configure.ac +++ b/configure.ac @@ -33,6 +33,27 @@ function checkDebugMode() { AC_SUBST(CFLAGS) } +function generate_wayland_protocols() { + RAYLIB_SRC_PATH="${srcdir}/raylib/raylib/src" + WL_PROTOCOLS_DIR="${RAYLIB_SRC_PATH}/external/glfw/deps/wayland" + AC_MSG_NOTICE([Generating Wayland protocol headers]) + wl_generate() { + protocol="$1" + basename="$2" + "$WAYLAND_SCANNER" client-header "$protocol" "$RAYLIB_SRC_PATH/$basename.h" || exit 1 + "$WAYLAND_SCANNER" private-code "$protocol" "$RAYLIB_SRC_PATH/$basename-code.h" || exit 1 + } + wl_generate "$WL_PROTOCOLS_DIR/wayland.xml" wayland-client-protocol + wl_generate "$WL_PROTOCOLS_DIR/xdg-shell.xml" xdg-shell-client-protocol + wl_generate "$WL_PROTOCOLS_DIR/xdg-decoration-unstable-v1.xml" xdg-decoration-unstable-v1-client-protocol + wl_generate "$WL_PROTOCOLS_DIR/viewporter.xml" viewporter-client-protocol + wl_generate "$WL_PROTOCOLS_DIR/relative-pointer-unstable-v1.xml" relative-pointer-unstable-v1-client-protocol + wl_generate "$WL_PROTOCOLS_DIR/pointer-constraints-unstable-v1.xml" pointer-constraints-unstable-v1-client-protocol + wl_generate "$WL_PROTOCOLS_DIR/fractional-scale-v1.xml" fractional-scale-v1-client-protocol + wl_generate "$WL_PROTOCOLS_DIR/xdg-activation-v1.xml" xdg-activation-v1-client-protocol + wl_generate "$WL_PROTOCOLS_DIR/idle-inhibit-unstable-v1.xml" idle-inhibit-unstable-v1-client-protocol +} + AC_ARG_WITH(mlpack, [AS_HELP_STRING([--with-mlpack], [Build the mlpack module])], [MLPACK="yes"], @@ -66,14 +87,22 @@ case "${host_os}" in *) PLATFORM_LDFLAGS="-Wl,--no-undefined -avoid-version" CLIPBOARD_LDFLAGS="`pkg-config xcb --libs` -lpthread" - NUKLEAR_LDFLAGS="-lGL -lm -lpthread -ldl -lrt -lX11" WEBSOCKET_LDFLAGS="" GTK_SERVER_LDFLAGS="`pkg-config --libs gtk+-3.0` -lXm -lXt" GTK_SERVER_CPPFLAGS="`pkg-config --cflags gtk+-3.0` -DGTK_SERVER_FFI -DGTK_SERVER_LIBRARY -DGTK_SERVER_UNIX -DGTK_SERVER_GTK3x" - RAYLIB_LDFLAGS="-lGL -lm -lpthread -ldl -lrt -lX11 -lwayland-client -lwayland-cursor -lwayland-egl -lxkbcommon" + RAYLIB_LDFLAGS="`pkg-config wayland-client wayland-cursor wayland-egl xkbcommon --libs`" JVM_CPPFLAGS="-I/usr/lib/jvm/java-1.8.0-openjdk-amd64/include -I/usr/lib/jvm/java-1.8.0-openjdk-amd64/include/linux" JVM_LDFLAGS="-L/usr/lib/jvm/java-1.8.0-openjdk-amd64/jre/lib/amd64/server -ljvm" - NUKLEAR_CPPFLAGS="-D_GLFW_X11=1" + NUKLEAR_CPPFLAGS="-D_GLFW_WAYLAND=1" + NUKLEAR_LDFLAGS="`pkg-config wayland-client wayland-cursor wayland-egl xkbcommon --libs`" + + AC_ARG_VAR([WAYLAND_SCANNER], [Path to wayland-scanner]) + AC_PATH_PROG([WAYLAND_SCANNER], [wayland-scanner]) + AS_IF([test -n "$WAYLAND_SCANNER"], [ + generate_wayland_protocols + ], [ + AC_MSG_WARN([wayland-scanner not found; Wayland support disabled]) + ]) esac AC_SUBST(DEBUG_LDFLAGS) diff --git a/glfw/Makefile.am b/glfw/Makefile.am index dc0dbaa..35ff783 100644 --- a/glfw/Makefile.am +++ b/glfw/Makefile.am @@ -7,6 +7,7 @@ AM_CXXFLAGS=-fno-rtti -std=c++14 AM_CPPFLAGS = \ + -I../raylib/raylib/src \ -I../raylib/raylib/src/external/glfw/include \ -I../raylib/raylib/src/external/glfw/deps \ -Wall -Wextra -Wshadow -Wdouble-promotion -Wno-unused-parameter -D_GLFW_BUILD_DLL=1 diff --git a/nuklear/Makefile.am b/nuklear/Makefile.am index 5b4f06f..d96ea60 100644 --- a/nuklear/Makefile.am +++ b/nuklear/Makefile.am @@ -7,6 +7,7 @@ AM_CXXFLAGS=-fno-rtti -std=c++14 AM_CPPFLAGS = -D_GLFW_BUILD_DLL=1 @NUKLEAR_CPPFLAGS@ \ + -I../raylib/raylib/src \ -I../raylib/raylib/src/external/glfw/include \ -I../raylib/raylib/src/external/glfw/deps lib_LTLIBRARIES = libnuklear.la diff --git a/nuklear/main.cpp b/nuklear/main.cpp index 9553b7c..f30a1f4 100644 --- a/nuklear/main.cpp +++ b/nuklear/main.cpp @@ -95,7 +95,7 @@ static void window_size_callback(GLFWwindow* window, int width, int height) { nk_context *nkp_create_window(const char *title, int width, int height) { if (!glfwInit()) { fprintf(stdout, "[GFLW] failed to init!\n"); - exit(1); + return nullptr; } glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2); @@ -873,16 +873,22 @@ static int cmd_widgetishovered(int argc, slib_par_t *params, var_t *retval) { } static int cmd_windowbegin(int argc, slib_par_t *params, var_t *retval) { - nkp_process_events(); - nkbd_begin(_ctx); - const char *title = get_param_str(argc, params, 0, "Untitled"); - struct nk_rect rc = get_param_rect(argc, params, 1); - nk_flags flags = get_param_window_flags(argc, params, 5); - v_setint(retval, nk_begin(_ctx, title, rc, flags)); - if ((flags & NK_WINDOW_TITLE) == 0) { - nkp_set_window_title(title); + int result; + if (_ctx != nullptr) { + nkp_process_events(); + nkbd_begin(_ctx); + const char *title = get_param_str(argc, params, 0, "Untitled"); + struct nk_rect rc = get_param_rect(argc, params, 1); + nk_flags flags = get_param_window_flags(argc, params, 5); + v_setint(retval, nk_begin(_ctx, title, rc, flags)); + if ((flags & NK_WINDOW_TITLE) == 0) { + nkp_set_window_title(title); + } + result = 1; + } else { + result = 0; } - return 1; + return result; } static int cmd_windowend(int argc, slib_par_t *params, var_t *retval) { From 9d9d017273737f3607b1eae827e2e9830f35e22f Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Tue, 20 Jan 2026 12:43:54 +1030 Subject: [PATCH 087/131] NUCLEAR: show window focused at startup --- nuklear/main.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nuklear/main.cpp b/nuklear/main.cpp index f30a1f4..d4ec06d 100644 --- a/nuklear/main.cpp +++ b/nuklear/main.cpp @@ -106,6 +106,8 @@ nk_context *nkp_create_window(const char *title, int width, int height) { title, nullptr, nullptr); glfwMakeContextCurrent(_window); + glfwSwapBuffers(_window); + gladLoadGL((GLADloadfunc) glfwGetProcAddress); glfwSetErrorCallback(error_callback); glfwSetWindowSizeCallback(_window, window_size_callback); From 44281e7b65837abcfb9f9dd89743e658a21517a6 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Fri, 23 Jan 2026 15:30:22 +1030 Subject: [PATCH 088/131] RAYLIB: allow make to depend on sbasic as part of flatpak processing --- raylib/Makefile.am | 11 +++- raylib/README.md | 51 +++++++++------ raylib/func-def.h | 14 +++- raylib/func.h | 152 ++++++++++++++++++++++++++++++++++++++------ raylib/mkraylib.bas | 2 +- raylib/mkreadme.bas | 2 +- raylib/proc-def.h | 3 + raylib/proc.h | 55 +++++++++++++--- raylib/raylib | 2 +- 9 files changed, 237 insertions(+), 55 deletions(-) diff --git a/raylib/Makefile.am b/raylib/Makefile.am index 875d5dc..0885662 100644 --- a/raylib/Makefile.am +++ b/raylib/Makefile.am @@ -8,8 +8,10 @@ generated = func-def.h proc-def.h proc.h func.h sbasic=sbasic +CLEANFILES = $(generated) + raylib/tools/rlparser/output/raylib_api.json: raylib/src/raylib.h raylib/tools/rlparser/rlparser.c - (cd raylib/tools/rlparser && make && ./rlparser --format JSON --input ../../src/raylib.h --output raylib_api.json) + (cd raylib/tools/rlparser && make && ./rlparser --format JSON --input ../../src/raylib.h --output output/raylib_api.json) UNSUPPORTED.md: $(generated) $(sbasic) mkraylib.bas unsupported > $@ @@ -21,7 +23,11 @@ $(generated): raylib/parser/raylib_api.json mkraylib.bas $(sbasic) mkraylib.bas $@ > $@ @touch main.cpp -gen: $(generated) README.md +gen: $(generated) + +gen: $(generated) + +all-am: $(generated) README.md AM_CXXFLAGS=-fno-rtti -std=c++14 -fpermissive AM_CPPFLAGS = -Iraylib/src -Iraylib/src/external/glfw/include -Iraylib/src/external/glfw/deps/mingw \ @@ -40,7 +46,6 @@ libraylib_la_SOURCES = \ raylib/src/rshapes.c \ raylib/src/rtextures.c \ raylib/src/rtext.c \ - raylib/src/utils.c \ ../include/param.cpp \ ../include/hashmap.cpp \ physac.cpp \ diff --git a/raylib/README.md b/raylib/README.md index b0f9723..08f508e 100644 --- a/raylib/README.md +++ b/raylib/README.md @@ -4,7 +4,7 @@ raylib is a simple and easy-to-use library to enjoy videogames programming. https://www.raylib.com/ -Implemented APIs (633) +Implemented APIs (646) ---------------- | Name | Description | @@ -16,7 +16,7 @@ Implemented APIs (633) | sub BeginScissorMode(x, y, width, height) | Begin scissor mode (define screen area for following drawing) | | sub BeginShaderMode(shader) | Begin custom shader drawing | | sub BeginTextureMode(target) | Begin drawing to render texture | -| func ChangeDirectory(dir) | Change working directory, return true on success | +| func ChangeDirectory(dirPath) | Change working directory, return true on success | | func CheckCollisionBoxes(box1, box2) | Check collision between two bounding boxes | | func CheckCollisionBoxSphere(box, center, radius) | Check collision between box and sphere | | func CheckCollisionCircleLine(center, radius, p1, p2) | Check if circle collides with a line created betweeen two points [p1] and [p2] | @@ -52,10 +52,11 @@ Implemented APIs (633) | func ComputeCRC32(data, dataSize) | Compute CRC32 hash code | | func ComputeMD5(data, dataSize) | Compute MD5 hash code, returns static int[4] (16 bytes) | | func ComputeSHA1(data, dataSize) | Compute SHA1 hash code, returns static int[5] (20 bytes) | +| func ComputeSHA256(data, dataSize) | Compute SHA256 hash code, returns static int[8] (32 bytes) | | func createPhysicsbodycircle() | n/a | | func createPhysicsbodypolygon() | n/a | | func createPhysicsbodyrectangle() | n/a | -| func DecodeDataBase64(data, outputSize) | Decode Base64 string data, memory must be MemFree() | +| func DecodeDataBase64(text, outputSize) | Decode Base64 string (expected NULL terminated), memory must be MemFree() | | func DecompressData(compData, compDataSize, dataSize) | Decompress data (DEFLATE algorithm), memory must be MemFree() | | func destroyPhysicsbody() | n/a | | func DirectoryExists(dirPath) | Check if a directory path exists | @@ -85,11 +86,14 @@ Implemented APIs (633) | sub DrawCylinderWiresEx(startPos, endPos, startRadius, endRadius, sides, color) | Draw a cylinder wires with base at startPos and top at endPos | | sub DrawEllipse(centerX, centerY, radiusH, radiusV, color) | Draw ellipse | | sub DrawEllipseLines(centerX, centerY, radiusH, radiusV, color) | Draw ellipse outline | +| sub DrawEllipseLinesV(center, radiusH, radiusV, color) | Draw ellipse outline (Vector version) | +| sub DrawEllipseV(center, radiusH, radiusV, color) | Draw ellipse (Vector version) | | sub DrawFPS(posX, posY) | Draw current FPS | | sub DrawGrid(slices, spacing) | Draw a grid (centered at (0, 0, 0)) | | sub DrawLine(startPosX, startPosY, endPosX, endPosY, color) | Draw a line | | sub DrawLine3D(startPos, endPos, color) | Draw a line in 3D world space | | sub DrawLineBezier(startPos, endPos, thick, color) | Draw line segment cubic-bezier in-out interpolation | +| sub DrawLineDashed(startPos, endPos, dashSize, spaceSize, color) | Draw a dashed line | | sub DrawLineEx(startPos, endPos, thick, color) | Draw a line (using triangles/quads) | | sub DrawLineStrip(points, pointCount, color) | Draw lines sequence (using gl lines) | | sub DrawLineV(startPos, endPos, color) | Draw a line (using gl lines) | @@ -108,7 +112,7 @@ Implemented APIs (633) | sub DrawPolyLinesEx(center, sides, radius, rotation, lineThick, color) | Draw a polygon outline of n sides with extended parameters | | sub DrawRay(ray, color) | Draw a ray line | | sub DrawRectangle(posX, posY, width, height, color) | Draw a color-filled rectangle | -| sub DrawRectangleGradientEx(rec, topLeft, bottomLeft, topRight, bottomRight) | Draw a gradient-filled rectangle with custom vertex colors | +| sub DrawRectangleGradientEx(rec, topLeft, bottomLeft, bottomRight, topRight) | Draw a gradient-filled rectangle with custom vertex colors | | sub DrawRectangleGradientH(posX, posY, width, height, left, right) | Draw a horizontal-gradient-filled rectangle | | sub DrawRectangleGradientV(posX, posY, width, height, top, bottom) | Draw a vertical-gradient-filled rectangle | | sub DrawRectangleLines(posX, posY, width, height, color) | Draw rectangle outline | @@ -153,7 +157,7 @@ Implemented APIs (633) | sub DrawTriangleStrip3D(points, pointCount, color) | Draw a triangle strip defined by points | | sub EnableCursor() | Enables cursor (unlock cursor) | | sub EnableEventWaiting() | Enable waiting for events on EndDrawing(), no automatic event polling | -| func EncodeDataBase64(data, dataSize, outputSize) | Encode data to Base64 string, memory must be MemFree() | +| func EncodeDataBase64(data, dataSize, outputSize) | Encode data to Base64 string (includes NULL terminator), memory must be MemFree() | | sub EndBlendMode() | End blending mode (reset to default: alpha blending) | | sub EndDrawing() | End canvas drawing and swap buffers (double buffering) | | sub EndMode2D() | Ends 2D mode with custom camera | @@ -173,7 +177,13 @@ Implemented APIs (633) | func ExportWave(wave, fileName) | Export wave data to file, returns true on success | | func ExportWaveAsCode(wave, fileName) | Export wave sample data to code (.h), returns true on success | | func Fade(color, alpha) | Get color with alpha applied, alpha goes from 0.0f to 1.0f | +| func FileCopy(srcPath, dstPath) | Copy file from one path to another, dstPath created if it doesn't exist | | func FileExists(fileName) | Check if file exists | +| func FileMove(srcPath, dstPath) | Move file from one directory to another, dstPath created if it doesn't exist | +| func FileRemove(fileName) | Remove file (if exists) | +| func FileRename(fileName, fileRename) | Rename file (if exists) | +| func FileTextFindIndex(fileName, search) | Find text in existing file | +| func FileTextReplace(fileName, search, replacement) | Replace text in an existing file | | func GenImageCellular(width, height, tileSize) | Generate image: cellular algorithm, bigger tileSize means bigger cells | | func GenImageChecked(width, height, checksX, checksY, col1, col2) | Generate image: checked | | func GenImageColor(width, height, color) | Generate image: plain color | @@ -218,8 +228,8 @@ Implemented APIs (633) | func GetFontDefault() | Get the default Font | | func GetFPS() | Get current FPS | | func GetFrameTime() | Get time in seconds for last frame drawn (delta time) | -| func GetGamepadAxisCount(gamepad) | Get gamepad axis count for a gamepad | -| func GetGamepadAxisMovement(gamepad, axis) | Get axis movement value for a gamepad axis | +| func GetGamepadAxisCount(gamepad) | Get axis count for a gamepad | +| func GetGamepadAxisMovement(gamepad, axis) | Get movement value for a gamepad axis | | func GetGamepadButtonPressed() | Get the last gamepad button pressed | | func GetGamepadName(gamepad) | Get gamepad internal name id | | func GetGestureDetected() | Get latest detected gesture | @@ -282,6 +292,7 @@ Implemented APIs (633) | func GetSplinePointBezierQuad(p1, c2, p3, t) | Get (evaluate) spline point: Quadratic Bezier | | func GetSplinePointCatmullRom(p1, p2, p3, p4, t) | Get (evaluate) spline point: Catmull-Rom | | func GetSplinePointLinear(startPos, endPos, t) | Get (evaluate) spline point: Linear | +| func GetTextBetween(text, begin, end) | Get text between two strings | | func GetTime() | Get elapsed time in seconds since InitWindow() | | func GetTouchPointCount() | Get number of touch points | | func GetTouchPointId(index) | Get touch point identifier for given index | @@ -396,7 +407,7 @@ Implemented APIs (633) | func IsCursorHidden() | Check if cursor is not visible | | func IsCursorOnScreen() | Check if cursor is on the screen | | func IsFileDropped() | Check if a file has been dropped into window | -| func IsFileExtension(fileName, ext) | Check file extension (including point: .png, .wav) | +| func IsFileExtension(fileName, ext) | Check file extension (recommended include point: .png, .wav) | | func IsFileNameValid(fileName) | Check if fileName is valid for the platform/OS | | func IsFontValid(font) | Check if a font is valid (font data loaded, WARNING: GPU texture not checked) | | func IsGamepadAvailable(gamepad) | Check if a gamepad is available | @@ -498,7 +509,7 @@ Implemented APIs (633) | func pollevents() | n/a | | sub PollInputEvents() | Register all input events | | func resetPhysics() | n/a | -| sub RestoreWindow() | Set window state: not minimized/maximized | +| sub RestoreWindow() | Restore window from being minimized/maximized | | sub ResumeAudioStream(stream) | Resume audio stream | | sub ResumeMusicStream(music) | Resume playing paused music | | sub ResumeSound(sound) | Resume a paused sound | @@ -525,7 +536,7 @@ Implemented APIs (633) | sub SetMouseOffset(offsetX, offsetY) | Set mouse offset | | sub SetMousePosition(x, y) | Set mouse position XY | | sub SetMouseScale(scaleX, scaleY) | Set mouse scaling | -| sub SetMusicPan(music, pan) | Set pan for a music (0.5 is center) | +| sub SetMusicPan(music, pan) | Set pan for a music (-1.0 left, 0.0 center, 1.0 right) | | sub SetMusicPitch(music, pitch) | Set pitch for a music (1.0 is base level) | | sub SetMusicVolume(music, volume) | Set volume for music (1.0 is max level) | | func setPhysicsbodyangularvelocity() | n/a | @@ -555,7 +566,7 @@ Implemented APIs (633) | sub SetShaderValueTexture(shader, locIndex, texture) | Set shader uniform value and bind the texture (sampler2d) | | sub SetShaderValueV(shader, locIndex, value, uniformType, count) | Set shader uniform value vector | | sub SetShapesTexture(texture, source) | Set texture and rectangle to be used on shapes drawing | -| sub SetSoundPan(sound, pan) | Set pan for a sound (0.5 is center) | +| sub SetSoundPan(sound, pan) | Set pan for a sound (-1.0 left, 0.0 center, 1.0 right) | | sub SetSoundPitch(sound, pitch) | Set pitch for a sound (1.0 is base level) | | sub SetSoundVolume(sound, volume) | Set volume for a sound (1.0 is max level) | | sub SetTargetFPS(fps) | Set target FPS (maximum) | @@ -582,14 +593,16 @@ Implemented APIs (633) | sub StopSound(sound) | Stop playing a sound | | sub SwapScreenBuffer() | Swap back buffer with front buffer (screen drawing) | | sub TakeScreenshot(fileName) | Takes a screenshot of current screen (filename extension defines format) | -| sub TextAppend(text, append, position) | Append text at specific position and move cursor! | +| sub TextAppend(text, append, position) | Append text at specific position and move cursor | | func TextCopy(dst, src) | Copy one string to another, returns bytes copied | -| func TextFindIndex(text, find) | Find first text occurrence within a string | +| func TextFindIndex(text, search) | Find first text occurrence within a string, -1 if not found | | func TextFormat(text, args) | Text formatting with variables (sprintf() style) | | func TextInsert(text, insert, position) | Insert text in a position (WARNING: memory must be freed!) | | func TextIsEqual(text1, text2) | Check if two text string are equal | | func TextLength(text) | Get text length, checks for '\\0' ending | -| func TextReplace(text, replace, by) | Replace text string (WARNING: memory must be freed!) | +| func TextRemoveSpaces(text) | Remove text spaces, concat words | +| func TextReplace(text, search, replacement) | Replace text string (WARNING: memory must be freed!) | +| func TextReplaceBetween(text, begin, end, replacement) | Replace text between two specific strings (WARNING: memory must be freed!) | | func TextSubtext(text, position, length) | Get a piece of a text string | | func TextToCamel(text) | Get Camel case notation version of provided string | | func TextToFloat(text) | Get float value from text | @@ -632,9 +645,9 @@ Implemented APIs (633) | sub UpdateModelAnimationBones(model, anim, frame) | Update model animation mesh bone matrices (GPU skinning) | | sub UpdateMusicStream(music) | Updates buffers for music streaming | | func updatePhysics() | n/a | -| sub UpdateSound(sound, data, sampleCount) | Update sound buffer with new data | -| sub UpdateTexture(texture, pixels) | Update GPU texture with new data | -| sub UpdateTextureRec(texture, rec, pixels) | Update GPU texture rectangle with new data | +| sub UpdateSound(sound, data, sampleCount) | Update sound buffer with new data (default data format: 32 bit float, stereo) | +| sub UpdateTexture(texture, pixels) | Update GPU texture with new data (pixels should be able to fill texture) | +| sub UpdateTextureRec(texture, rec, pixels) | Update GPU texture rectangle with new data (pixels and rec should fit in texture) | | sub UploadMesh(mesh, dynamic) | Upload mesh vertex data in GPU and provide VAO/VBO ids | | func waitevents() | n/a | | sub WaitTime(seconds) | Wait for some time (halt program execution) | @@ -661,6 +674,7 @@ Unimplemented APIs | LoadFontData | Load font data for further use | | LoadMaterialDefault | Load default material (Supports: DIFFUSE, SPECULAR, NORMAL maps) | | LoadMaterials | Load materials from model file | +| LoadTextLines | Load text as separate lines ('\\n') | | LoadVrStereoConfig | Load VR stereo config for VR simulator device parameters | | SetAudioStreamCallback | Audio thread callback to request new data | | SetLoadFileDataCallback | Set custom file binary data loader | @@ -670,8 +684,9 @@ Unimplemented APIs | SetSaveFileTextCallback | Set custom file text data saver | | SetTraceLogCallback | Set custom trace log | | TextJoin | Join text strings with delimiter | -| TextSplit | Split text into multiple strings | +| TextSplit | Split text into multiple strings, using MAX_TEXTSPLIT_COUNT static strings | | UnloadFontData | Unload font chars info data (RAM) | | UnloadMaterial | Unload material from GPU memory (VRAM) | +| UnloadTextLines | Unload text lines | | UnloadVrStereoConfig | Unload VR stereo config | diff --git a/raylib/func-def.h b/raylib/func-def.h index 831f691..42552b2 100644 --- a/raylib/func-def.h +++ b/raylib/func-def.h @@ -29,6 +29,7 @@ {2, 2, "COMPUTECRC32", cmd_computecrc32}, {2, 2, "COMPUTEMD5", cmd_computemd5}, {2, 2, "COMPUTESHA1", cmd_computesha1}, + {2, 2, "COMPUTESHA256", cmd_computesha256}, {1, 1, "DECODEDATABASE64", cmd_decodedatabase64}, {2, 2, "DECOMPRESSDATA", cmd_decompressdata}, {1, 1, "DIRECTORYEXISTS", cmd_directoryexists}, @@ -44,7 +45,13 @@ {2, 2, "EXPORTWAVE", cmd_exportwave}, {2, 2, "EXPORTWAVEASCODE", cmd_exportwaveascode}, {2, 2, "FADE", cmd_fade}, + {2, 2, "FILECOPY", cmd_filecopy}, {1, 1, "FILEEXISTS", cmd_fileexists}, + {2, 2, "FILEMOVE", cmd_filemove}, + {1, 1, "FILEREMOVE", cmd_fileremove}, + {2, 2, "FILERENAME", cmd_filerename}, + {2, 2, "FILETEXTFINDINDEX", cmd_filetextfindindex}, + {3, 3, "FILETEXTREPLACE", cmd_filetextreplace}, {3, 3, "GENIMAGECELLULAR", cmd_genimagecellular}, {6, 6, "GENIMAGECHECKED", cmd_genimagechecked}, {3, 3, "GENIMAGECOLOR", cmd_genimagecolor}, @@ -146,6 +153,7 @@ {4, 4, "GETSPLINEPOINTBEZIERQUAD", cmd_getsplinepointbezierquad}, {5, 5, "GETSPLINEPOINTCATMULLROM", cmd_getsplinepointcatmullrom}, {3, 3, "GETSPLINEPOINTLINEAR", cmd_getsplinepointlinear}, + {3, 3, "GETTEXTBETWEEN", cmd_gettextbetween}, {0, 0, "GETTIME", cmd_gettime}, {0, 0, "GETTOUCHPOINTCOUNT", cmd_gettouchpointcount}, {1, 1, "GETTOUCHPOINTID", cmd_gettouchpointid}, @@ -218,9 +226,9 @@ {1, 1, "LOADFILEDATA", cmd_loadfiledata}, {1, 1, "LOADFILETEXT", cmd_loadfiletext}, {1, 1, "LOADFONT", cmd_loadfont}, - {3, 3, "LOADFONTEX", cmd_loadfontex}, + {4, 4, "LOADFONTEX", cmd_loadfontex}, {3, 3, "LOADFONTFROMIMAGE", cmd_loadfontfromimage}, - {5, 5, "LOADFONTFROMMEMORY", cmd_loadfontfrommemory}, + {6, 6, "LOADFONTFROMMEMORY", cmd_loadfontfrommemory}, {1, 1, "LOADIMAGE", cmd_loadimage}, {1, 1, "LOADIMAGEANIM", cmd_loadimageanim}, {3, 3, "LOADIMAGEANIMFROMMEMORY", cmd_loadimageanimfrommemory}, @@ -259,7 +267,9 @@ {3, 3, "TEXTINSERT", cmd_textinsert}, {2, 2, "TEXTISEQUAL", cmd_textisequal}, {1, 1, "TEXTLENGTH", cmd_textlength}, + {1, 1, "TEXTREMOVESPACES", cmd_textremovespaces}, {3, 3, "TEXTREPLACE", cmd_textreplace}, + {4, 4, "TEXTREPLACEBETWEEN", cmd_textreplacebetween}, {3, 3, "TEXTSUBTEXT", cmd_textsubtext}, {1, 1, "TEXTTOCAMEL", cmd_texttocamel}, {1, 1, "TEXTTOFLOAT", cmd_texttofloat}, diff --git a/raylib/func.h b/raylib/func.h index cc97d4a..53ccee6 100644 --- a/raylib/func.h +++ b/raylib/func.h @@ -2,8 +2,8 @@ // Change working directory, return true on success // static int cmd_changedirectory(int argc, slib_par_t *params, var_t *retval) { - auto dir = get_param_str(argc, params, 0, 0); - auto fnResult = ChangeDirectory(dir); + auto dirPath = get_param_str(argc, params, 0, 0); + auto fnResult = ChangeDirectory(dirPath); v_setint(retval, fnResult); return 1; } @@ -358,12 +358,23 @@ static int cmd_computesha1(int argc, slib_par_t *params, var_t *retval) { } // -// Decode Base64 string data, memory must be MemFree() +// Compute SHA256 hash code, returns static int[8] (32 bytes) +// +static int cmd_computesha256(int argc, slib_par_t *params, var_t *retval) { + auto data = (unsigned char *)get_param_str(argc, params, 0, 0); + auto dataSize = get_param_int(argc, params, 1, 0); + auto fnResult = (var_int_t)ComputeSHA256(data, dataSize); + v_setint(retval, fnResult); + return 1; +} + +// +// Decode Base64 string (expected NULL terminated), memory must be MemFree() // static int cmd_decodedatabase64(int argc, slib_par_t *params, var_t *retval) { - auto data = (const unsigned char *)get_param_str(argc, params, 0, 0); + auto text = get_param_str(argc, params, 0, 0); auto outputSize = 0; - auto fnResult = (const char *)DecodeDataBase64(data, &outputSize); + auto fnResult = (const char *)DecodeDataBase64(text, &outputSize); v_setstrn(retval, fnResult, outputSize); MemFree((void *)fnResult); return 1; @@ -393,7 +404,7 @@ static int cmd_directoryexists(int argc, slib_par_t *params, var_t *retval) { } // -// Encode data to Base64 string, memory must be MemFree() +// Encode data to Base64 string (includes NULL terminator), memory must be MemFree() // static int cmd_encodedatabase64(int argc, slib_par_t *params, var_t *retval) { auto data = (const unsigned char *)get_param_str(argc, params, 0, 0); @@ -583,6 +594,17 @@ static int cmd_fade(int argc, slib_par_t *params, var_t *retval) { return 1; } +// +// Copy file from one path to another, dstPath created if it doesn't exist +// +static int cmd_filecopy(int argc, slib_par_t *params, var_t *retval) { + auto srcPath = get_param_str(argc, params, 0, 0); + auto dstPath = get_param_str(argc, params, 1, 0); + auto fnResult = FileCopy(srcPath, dstPath); + v_setint(retval, fnResult); + return 1; +} + // // Check if file exists // @@ -593,6 +615,61 @@ static int cmd_fileexists(int argc, slib_par_t *params, var_t *retval) { return 1; } +// +// Move file from one directory to another, dstPath created if it doesn't exist +// +static int cmd_filemove(int argc, slib_par_t *params, var_t *retval) { + auto srcPath = get_param_str(argc, params, 0, 0); + auto dstPath = get_param_str(argc, params, 1, 0); + auto fnResult = FileMove(srcPath, dstPath); + v_setint(retval, fnResult); + return 1; +} + +// +// Remove file (if exists) +// +static int cmd_fileremove(int argc, slib_par_t *params, var_t *retval) { + auto fileName = get_param_str(argc, params, 0, 0); + auto fnResult = FileRemove(fileName); + v_setint(retval, fnResult); + return 1; +} + +// +// Rename file (if exists) +// +static int cmd_filerename(int argc, slib_par_t *params, var_t *retval) { + auto fileName = get_param_str(argc, params, 0, 0); + auto fileRename = get_param_str(argc, params, 1, 0); + auto fnResult = FileRename(fileName, fileRename); + v_setint(retval, fnResult); + return 1; +} + +// +// Find text in existing file +// +static int cmd_filetextfindindex(int argc, slib_par_t *params, var_t *retval) { + auto fileName = get_param_str(argc, params, 0, 0); + auto search = get_param_str(argc, params, 1, 0); + auto fnResult = FileTextFindIndex(fileName, search); + v_setint(retval, fnResult); + return 1; +} + +// +// Replace text in an existing file +// +static int cmd_filetextreplace(int argc, slib_par_t *params, var_t *retval) { + auto fileName = get_param_str(argc, params, 0, 0); + auto search = get_param_str(argc, params, 1, 0); + auto replacement = get_param_str(argc, params, 2, 0); + auto fnResult = FileTextReplace(fileName, search, replacement); + v_setint(retval, fnResult); + return 1; +} + // // Generate image: cellular algorithm, bigger tileSize means bigger cells // @@ -1073,7 +1150,7 @@ static int cmd_getframetime(int argc, slib_par_t *params, var_t *retval) { } // -// Get gamepad axis count for a gamepad +// Get axis count for a gamepad // static int cmd_getgamepadaxiscount(int argc, slib_par_t *params, var_t *retval) { auto gamepad = get_param_int(argc, params, 0, 0); @@ -1083,7 +1160,7 @@ static int cmd_getgamepadaxiscount(int argc, slib_par_t *params, var_t *retval) } // -// Get axis movement value for a gamepad axis +// Get movement value for a gamepad axis // static int cmd_getgamepadaxismovement(int argc, slib_par_t *params, var_t *retval) { auto gamepad = get_param_int(argc, params, 0, 0); @@ -1734,6 +1811,18 @@ static int cmd_getsplinepointlinear(int argc, slib_par_t *params, var_t *retval) return 1; } +// +// Get text between two strings +// +static int cmd_gettextbetween(int argc, slib_par_t *params, var_t *retval) { + auto text = get_param_str(argc, params, 0, 0); + auto begin = get_param_str(argc, params, 1, 0); + auto end = get_param_str(argc, params, 2, 0); + auto fnResult = (const char *)GetTextBetween(text, begin, end); + v_setstr(retval, fnResult); + return 1; +} + // // Get elapsed time in seconds since InitWindow() // @@ -2028,7 +2117,7 @@ static int cmd_isfiledropped(int argc, slib_par_t *params, var_t *retval) { } // -// Check file extension (including point: .png, .wav) +// Check file extension (recommended include point: .png, .wav) // static int cmd_isfileextension(int argc, slib_par_t *params, var_t *retval) { auto fileName = get_param_str(argc, params, 0, 0); @@ -2575,8 +2664,8 @@ static int cmd_loadfont(int argc, slib_par_t *params, var_t *retval) { static int cmd_loadfontex(int argc, slib_par_t *params, var_t *retval) { auto fileName = get_param_str(argc, params, 0, 0); auto fontSize = get_param_int(argc, params, 1, 0); - auto codepoints = (int *)0; - auto codepointCount = get_param_int(argc, params, 2, 0); + auto codepoints = (const int *)get_param_int_t(argc, params, 2, 0); + auto codepointCount = get_param_int(argc, params, 3, 0); auto fnResult = LoadFontEx(fileName, fontSize, codepoints, codepointCount); v_setfont(retval, fnResult); return 1; @@ -2608,8 +2697,8 @@ static int cmd_loadfontfrommemory(int argc, slib_par_t *params, var_t *retval) { auto fileData = (const unsigned char *)get_param_str(argc, params, 1, 0); auto dataSize = get_param_int(argc, params, 2, 0); auto fontSize = get_param_int(argc, params, 3, 0); - auto codepoints = (int *)0; - auto codepointCount = get_param_int(argc, params, 4, 0); + auto codepoints = (const int *)get_param_int_t(argc, params, 4, 0); + auto codepointCount = get_param_int(argc, params, 5, 0); auto fnResult = LoadFontFromMemory(fileType, fileData, dataSize, fontSize, codepoints, codepointCount); v_setfont(retval, fnResult); return 1; @@ -3017,7 +3106,7 @@ static int cmd_savefiledata(int argc, slib_par_t *params, var_t *retval) { // static int cmd_savefiletext(int argc, slib_par_t *params, var_t *retval) { auto fileName = get_param_str(argc, params, 0, 0); - auto text = (char *)get_param_str(argc, params, 1, 0); + auto text = get_param_str(argc, params, 1, 0); auto fnResult = SaveFileText(fileName, text); v_setint(retval, fnResult); return 1; @@ -3045,12 +3134,12 @@ static int cmd_textcopy(int argc, slib_par_t *params, var_t *retval) { } // -// Find first text occurrence within a string +// Find first text occurrence within a string, -1 if not found // static int cmd_textfindindex(int argc, slib_par_t *params, var_t *retval) { auto text = get_param_str(argc, params, 0, 0); - auto find = get_param_str(argc, params, 1, 0); - auto fnResult = TextFindIndex(text, find); + auto search = get_param_str(argc, params, 1, 0); + auto fnResult = TextFindIndex(text, search); v_setint(retval, fnResult); return 1; } @@ -3088,14 +3177,37 @@ static int cmd_textlength(int argc, slib_par_t *params, var_t *retval) { return 1; } +// +// Remove text spaces, concat words +// +static int cmd_textremovespaces(int argc, slib_par_t *params, var_t *retval) { + auto text = get_param_str(argc, params, 0, 0); + auto fnResult = (const char *)TextRemoveSpaces(text); + v_setstr(retval, fnResult); + return 1; +} + // // Replace text string (WARNING: memory must be freed!) // static int cmd_textreplace(int argc, slib_par_t *params, var_t *retval) { auto text = get_param_str(argc, params, 0, 0); - auto replace = get_param_str(argc, params, 1, 0); - auto by = get_param_str(argc, params, 2, 0); - auto fnResult = (const char *)TextReplace(text, replace, by); + auto search = get_param_str(argc, params, 1, 0); + auto replacement = get_param_str(argc, params, 2, 0); + auto fnResult = (const char *)TextReplace(text, search, replacement); + v_setstr(retval, fnResult); + return 1; +} + +// +// Replace text between two specific strings (WARNING: memory must be freed!) +// +static int cmd_textreplacebetween(int argc, slib_par_t *params, var_t *retval) { + auto text = get_param_str(argc, params, 0, 0); + auto begin = get_param_str(argc, params, 1, 0); + auto end = get_param_str(argc, params, 2, 0); + auto replacement = get_param_str(argc, params, 3, 0); + auto fnResult = (const char *)TextReplaceBetween(text, begin, end, replacement); v_setstr(retval, fnResult); return 1; } diff --git a/raylib/mkraylib.bas b/raylib/mkraylib.bas index ee64008..e37d4c5 100644 --- a/raylib/mkraylib.bas +++ b/raylib/mkraylib.bas @@ -2,7 +2,7 @@ rem rem generate a skelton main.cpp from json input rem -tload "raylib/parser/raylib_api.json", s, 1 +tload "raylib/tools/rlparser/output/raylib_api.json", s, 1 api = array(s) func comparator(l, r) diff --git a/raylib/mkreadme.bas b/raylib/mkreadme.bas index 56230b9..0f98b50 100644 --- a/raylib/mkreadme.bas +++ b/raylib/mkreadme.bas @@ -20,7 +20,7 @@ load("main.cpp") load("proc-def.h") load("func-def.h") -tload "raylib/parser/raylib_api.json", s, 1 +tload "raylib/tools/rlparser/output/raylib_api.json", s, 1 api = array(s) functions = {} for fun in api("functions") diff --git a/raylib/proc-def.h b/raylib/proc-def.h index f509533..e4456bf 100644 --- a/raylib/proc-def.h +++ b/raylib/proc-def.h @@ -35,11 +35,14 @@ {6, 6, "DRAWCYLINDERWIRESEX", cmd_drawcylinderwiresex}, {5, 5, "DRAWELLIPSE", cmd_drawellipse}, {5, 5, "DRAWELLIPSELINES", cmd_drawellipselines}, + {4, 4, "DRAWELLIPSELINESV", cmd_drawellipselinesv}, + {4, 4, "DRAWELLIPSEV", cmd_drawellipsev}, {2, 2, "DRAWFPS", cmd_drawfps}, {2, 2, "DRAWGRID", cmd_drawgrid}, {5, 5, "DRAWLINE", cmd_drawline}, {3, 3, "DRAWLINE3D", cmd_drawline3d}, {4, 4, "DRAWLINEBEZIER", cmd_drawlinebezier}, + {5, 5, "DRAWLINEDASHED", cmd_drawlinedashed}, {4, 4, "DRAWLINEEX", cmd_drawlineex}, {3, 3, "DRAWLINESTRIP", cmd_drawlinestrip}, {3, 3, "DRAWLINEV", cmd_drawlinev}, diff --git a/raylib/proc.h b/raylib/proc.h index ce6790c..f549c73 100644 --- a/raylib/proc.h +++ b/raylib/proc.h @@ -449,6 +449,30 @@ static int cmd_drawellipselines(int argc, slib_par_t *params, var_t *retval) { return 1; } +// +// Draw ellipse outline (Vector version) +// +static int cmd_drawellipselinesv(int argc, slib_par_t *params, var_t *retval) { + auto center = get_param_vec2(argc, params, 0); + auto radiusH = get_param_num(argc, params, 1, 0); + auto radiusV = get_param_num(argc, params, 2, 0); + auto color = get_param_color(argc, params, 3); + DrawEllipseLinesV(center, radiusH, radiusV, color); + return 1; +} + +// +// Draw ellipse (Vector version) +// +static int cmd_drawellipsev(int argc, slib_par_t *params, var_t *retval) { + auto center = get_param_vec2(argc, params, 0); + auto radiusH = get_param_num(argc, params, 1, 0); + auto radiusV = get_param_num(argc, params, 2, 0); + auto color = get_param_color(argc, params, 3); + DrawEllipseV(center, radiusH, radiusV, color); + return 1; +} + // // Draw current FPS // @@ -505,6 +529,19 @@ static int cmd_drawlinebezier(int argc, slib_par_t *params, var_t *retval) { return 1; } +// +// Draw a dashed line +// +static int cmd_drawlinedashed(int argc, slib_par_t *params, var_t *retval) { + auto startPos = get_param_vec2(argc, params, 0); + auto endPos = get_param_vec2(argc, params, 1); + auto dashSize = get_param_int(argc, params, 2, 0); + auto spaceSize = get_param_int(argc, params, 3, 0); + auto color = get_param_color(argc, params, 4); + DrawLineDashed(startPos, endPos, dashSize, spaceSize, color); + return 1; +} + // // Draw a line (using triangles/quads) // @@ -765,9 +802,9 @@ static int cmd_drawrectanglegradientex(int argc, slib_par_t *params, var_t *retv auto rec = get_param_rect(argc, params, 0); auto topLeft = get_param_color(argc, params, 1); auto bottomLeft = get_param_color(argc, params, 2); - auto topRight = get_param_color(argc, params, 3); - auto bottomRight = get_param_color(argc, params, 4); - DrawRectangleGradientEx(rec, topLeft, bottomLeft, topRight, bottomRight); + auto bottomRight = get_param_color(argc, params, 3); + auto topRight = get_param_color(argc, params, 4); + DrawRectangleGradientEx(rec, topLeft, bottomLeft, bottomRight, topRight); return 1; } @@ -2427,7 +2464,7 @@ static int cmd_pollinputevents(int argc, slib_par_t *params, var_t *retval) { } // -// Set window state: not minimized/maximized +// Restore window from being minimized/maximized // static int cmd_restorewindow(int argc, slib_par_t *params, var_t *retval) { RestoreWindow(); @@ -2684,7 +2721,7 @@ static int cmd_setmousescale(int argc, slib_par_t *params, var_t *retval) { } // -// Set pan for a music (0.5 is center) +// Set pan for a music (-1.0 left, 0.0 center, 1.0 right) // static int cmd_setmusicpan(int argc, slib_par_t *params, var_t *retval) { int result; @@ -2815,7 +2852,7 @@ static int cmd_setshapestexture(int argc, slib_par_t *params, var_t *retval) { } // -// Set pan for a sound (0.5 is center) +// Set pan for a sound (-1.0 left, 0.0 center, 1.0 right) // static int cmd_setsoundpan(int argc, slib_par_t *params, var_t *retval) { int result; @@ -3123,7 +3160,7 @@ static int cmd_takescreenshot(int argc, slib_par_t *params, var_t *retval) { } // -// Append text at specific position and move cursor! +// Append text at specific position and move cursor // static int cmd_textappend(int argc, slib_par_t *params, var_t *retval) { auto text = (char *)get_param_str(argc, params, 0, 0); @@ -3543,7 +3580,7 @@ static int cmd_updatemusicstream(int argc, slib_par_t *params, var_t *retval) { } // -// Update sound buffer with new data +// Update sound buffer with new data (default data format: 32 bit float, stereo) // static int cmd_updatesound(int argc, slib_par_t *params, var_t *retval) { int result; @@ -3560,7 +3597,7 @@ static int cmd_updatesound(int argc, slib_par_t *params, var_t *retval) { } // -// Update GPU texture rectangle with new data +// Update GPU texture rectangle with new data (pixels and rec should fit in texture) // static int cmd_updatetexturerec(int argc, slib_par_t *params, var_t *retval) { int result; diff --git a/raylib/raylib b/raylib/raylib index eb8a343..c610d22 160000 --- a/raylib/raylib +++ b/raylib/raylib @@ -1 +1 @@ -Subproject commit eb8a343e313967a51cb302ac9bb1206a05727d13 +Subproject commit c610d228a244f930ad53492604640f39584c66da From e2ae4b9005ab2da38323a6c3c15caab2f291f0ed Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Fri, 23 Jan 2026 16:53:06 +1030 Subject: [PATCH 089/131] COMMON: fix autogen.sh --- autogen.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/autogen.sh b/autogen.sh index 47736e2..318d661 100755 --- a/autogen.sh +++ b/autogen.sh @@ -1,3 +1,5 @@ +#!/bin/bash + # This file is part of SmallBASIC # # Copyright(C) 2001-2020 Chris Warren-Smith. From 0967b93b86c24901f3ad5638492f0127db894d9c Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Fri, 23 Jan 2026 17:06:47 +1030 Subject: [PATCH 090/131] RAYLIB: fix reference to raylib_api.json --- raylib/Makefile.am | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/raylib/Makefile.am b/raylib/Makefile.am index 0885662..84c24b5 100644 --- a/raylib/Makefile.am +++ b/raylib/Makefile.am @@ -5,12 +5,13 @@ # Download the GNU Public License (GPL) from www.gnu.org # -generated = func-def.h proc-def.h proc.h func.h sbasic=sbasic +generated = func-def.h proc-def.h proc.h func.h +raylib_api_json=raylib/tools/rlparser/output/raylib_api.json CLEANFILES = $(generated) -raylib/tools/rlparser/output/raylib_api.json: raylib/src/raylib.h raylib/tools/rlparser/rlparser.c +$(raylib_api_json): raylib/src/raylib.h raylib/tools/rlparser/rlparser.c (cd raylib/tools/rlparser && make && ./rlparser --format JSON --input ../../src/raylib.h --output output/raylib_api.json) UNSUPPORTED.md: $(generated) @@ -19,7 +20,7 @@ UNSUPPORTED.md: $(generated) README.md: $(generated) mkreadme.bas UNSUPPORTED.md $(sbasic) mkreadme.bas `grep RAYLIB_VERSION raylib/src/raylib.h | sed 's/#define RAYLIB_VERSION//g' | sed 's/\"//g'` > README.md -$(generated): raylib/parser/raylib_api.json mkraylib.bas +$(generated): $(raylib_api_json) mkraylib.bas $(sbasic) mkraylib.bas $@ > $@ @touch main.cpp From 7a268902a1b88f84ca2d314e63312923b7d84b49 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Mon, 9 Feb 2026 08:59:24 +1030 Subject: [PATCH 091/131] LLAMA: add support for grammar --- llama/llama-sb.cpp | 55 ++++++++++++++++++++++++++++++++-------------- llama/llama-sb.h | 7 +++++- llama/main.cpp | 41 +++++++++++++++++++++++++++++++++- 3 files changed, 85 insertions(+), 18 deletions(-) diff --git a/llama/llama-sb.cpp b/llama/llama-sb.cpp index 2bff5e8..273bf49 100644 --- a/llama/llama-sb.cpp +++ b/llama/llama-sb.cpp @@ -31,7 +31,8 @@ Llama::Llama() : _min_p(0), _top_k(0), _max_tokens(0), - _log_level(GGML_LOG_LEVEL_CONT) { + _log_level(GGML_LOG_LEVEL_CONT), + _seed(LLAMA_DEFAULT_SEED) { llama_log_set([](enum ggml_log_level level, const char * text, void *user_data) { Llama *llama = (Llama *)user_data; if (level > llama->_log_level) { @@ -63,6 +64,9 @@ void Llama::reset() { _top_p = 1.0f; _min_p = 0.0f; _max_tokens = 150; + _grammar_src.clear(); + _grammar_root.clear(); + _seed = LLAMA_DEFAULT_SEED; if (_ctx) { llama_memory_clear(llama_get_memory(_ctx), true); } @@ -93,36 +97,53 @@ bool Llama::construct(string model_path, int n_ctx, int n_batch, int n_gpu_layer _last_error = "Failed to create context"; } else { _vocab = llama_model_get_vocab(_model); - - auto sparams = llama_sampler_chain_default_params(); - sparams.no_perf = false; - _sampler = llama_sampler_chain_init(sparams); } } return _last_error.empty(); } -void Llama::configure_sampler() { - llama_sampler_reset(_sampler); +void Llama::set_grammar(const string &src, const string &root) { + _grammar_src = src; + _grammar_root = root; +} + +bool Llama::configure_sampler() { + auto sparams = llama_sampler_chain_default_params(); + sparams.no_perf = false; + llama_sampler *chain = llama_sampler_chain_init(sparams); + + if (!_grammar_src.empty()) { + llama_sampler *grammar = llama_sampler_init_grammar(_vocab, _grammar_src.c_str(), _grammar_root.c_str()); + if (!grammar) { + _last_error = "failed to initialize grammar sampler"; + return false; + } + llama_sampler_chain_add(chain, grammar); + } if (_penalty_last_n != 0 && _penalty_repeat != 1.0f) { auto penalties = llama_sampler_init_penalties(_penalty_last_n, _penalty_repeat, 0.0f, 0.0f); - llama_sampler_chain_add(_sampler, penalties); + llama_sampler_chain_add(chain, penalties); } if (_temperature <= 0.0f) { - llama_sampler_chain_add(_sampler, llama_sampler_init_greedy()); + llama_sampler_chain_add(chain, llama_sampler_init_greedy()); } else { - llama_sampler_chain_add(_sampler, llama_sampler_init_temp(_temperature)); if (_top_k > 0) { - llama_sampler_chain_add(_sampler, llama_sampler_init_top_k(_top_k)); + llama_sampler_chain_add(chain, llama_sampler_init_top_k(_top_k)); } - if (_top_p < 1.0f) { - llama_sampler_chain_add(_sampler, llama_sampler_init_top_p(_top_p, 1)); + if (_top_p < 1.0f || _min_p > 0.0f) { + llama_sampler_chain_add(chain, llama_sampler_init_top_p(_top_p, 1)); } if (_min_p > 0.0f) { - llama_sampler_chain_add(_sampler, llama_sampler_init_min_p(_min_p, 1)); + llama_sampler_chain_add(chain, llama_sampler_init_min_p(_min_p, 1)); } - llama_sampler_chain_add(_sampler, llama_sampler_init_dist(LLAMA_DEFAULT_SEED)); + llama_sampler_chain_add(chain, llama_sampler_init_temp(_temperature)); + llama_sampler_chain_add(chain, llama_sampler_init_dist(_seed)); } + if (_sampler) { + llama_sampler_free(_sampler); + } + _sampler = chain; + return true; } vector Llama::tokenize(const string &prompt) { @@ -201,7 +222,9 @@ bool Llama::make_space_for_tokens(int n_tokens, int keep_min) { } bool Llama::generate(LlamaIter &iter, const string &prompt) { - configure_sampler(); + if (!configure_sampler()) { + return false; + } vector prompt_tokens = tokenize(prompt); if (prompt_tokens.size() == 0) { diff --git a/llama/llama-sb.h b/llama/llama-sb.h index b1da148..070cb60 100644 --- a/llama/llama-sb.h +++ b/llama/llama-sb.h @@ -50,6 +50,8 @@ struct Llama { void set_temperature(float temperature) { _temperature = temperature; } void set_top_k(int top_k) { _top_k = top_k; } void set_top_p(float top_p) { _top_p = top_p; } + void set_grammar(const string &src, const string &root); + void set_seed(unsigned int seed) { _seed = seed; } // error handling const char *last_error() { return _last_error.c_str(); } @@ -58,7 +60,7 @@ struct Llama { private: bool ends_with_sentence_boundary(const string &out); - void configure_sampler(); + bool configure_sampler(); bool make_space_for_tokens(int n_tokens, int keep_min); vector tokenize(const string &prompt); string token_to_string(LlamaIter &iter, llama_token tok); @@ -68,6 +70,8 @@ struct Llama { llama_sampler *_sampler; const llama_vocab *_vocab; vector _stop_sequences; + string _grammar_src; + string _grammar_root; string _last_error; int32_t _penalty_last_n; float _penalty_repeat; @@ -77,4 +81,5 @@ struct Llama { int _top_k; int _max_tokens; int _log_level; + unsigned int _seed; }; diff --git a/llama/main.cpp b/llama/main.cpp index 1c0de21..5bddfed 100644 --- a/llama/main.cpp +++ b/llama/main.cpp @@ -211,6 +211,42 @@ static int cmd_llama_set_top_p(var_s *self, int argc, slib_par_t *arg, var_s *re return result; } +// +// llama.set_grammar("text") +// +static int cmd_llama_set_grammar(var_s *self, int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + if (argc != 1) { + error(retval, "llama.set_grammar", 1, 1); + } else { + int id = get_llama_class_id(self, retval); + if (id != -1) { + Llama &llama = g_llama.at(id); + llama.set_grammar(get_param_str(argc, arg, 0, 0), "root"); + result = 1; + } + } + return result; +} + +// +// llama.set_seed(123) +// +static int cmd_llama_set_seed(var_s *self, int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + if (argc != 1) { + error(retval, "llama.set_seed", 1, 1); + } else { + int id = get_llama_class_id(self, retval); + if (id != -1) { + Llama &llama = g_llama.at(id); + llama.set_seed(get_param_num(argc, arg, 0, 0)); + result = 1; + } + } + return result; +} + // // llama.reset() - make the model forget everything // @@ -355,6 +391,8 @@ static int cmd_create_llama(int argc, slib_par_t *params, var_t *retval) { v_create_callback(retval, "set_temperature", cmd_llama_set_temperature); v_create_callback(retval, "set_top_k", cmd_llama_set_top_k); v_create_callback(retval, "set_top_p", cmd_llama_set_top_p); + v_create_callback(retval, "set_grammar", cmd_llama_set_grammar); + v_create_callback(retval, "set_seed", cmd_llama_set_seed); result = 1; } else { error(retval, llama.last_error()); @@ -388,7 +426,7 @@ int sblib_init(const char *sourceFile) { // // Release variables falling out of scope // -SBLIB_API void sblib_free(int cls_id, int id) { +SBLIB_API int sblib_free(int cls_id, int id) { if (id != -1) { switch (cls_id) { case CLASS_ID_LLAMA: @@ -403,6 +441,7 @@ SBLIB_API void sblib_free(int cls_id, int id) { break; } } + return 0; } // From feb6e6f17a8e69fe7c5f5d046e00ccb3654804a5 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Sat, 18 Apr 2026 16:13:13 +0930 Subject: [PATCH 092/131] LLAMA: implement nitro agent (work in progress) --- llama/llama-sb.cpp | 7 +- llama/llama-sb.h | 2 +- llama/llama.cpp | 2 +- llama/main.cpp | 5 +- llama/samples/nitro_cli.bas | 193 ++++++++++++++++++++++++++++++++++++ llama/samples/skills.md | 139 ++++++++++++++++++++++++++ llama/test_main.cpp | 2 +- 7 files changed, 343 insertions(+), 7 deletions(-) create mode 100644 llama/samples/nitro_cli.bas create mode 100644 llama/samples/skills.md diff --git a/llama/llama-sb.cpp b/llama/llama-sb.cpp index 273bf49..0325509 100644 --- a/llama/llama-sb.cpp +++ b/llama/llama-sb.cpp @@ -40,6 +40,7 @@ Llama::Llama() : } }, this); reset(); + llama_backend_init(); } Llama::~Llama() { @@ -52,6 +53,7 @@ Llama::~Llama() { if (_model) { llama_model_free(_model); } + llama_backend_free(); } void Llama::reset() { @@ -72,14 +74,15 @@ void Llama::reset() { } } -bool Llama::construct(string model_path, int n_ctx, int n_batch, int n_gpu_layers) { +bool Llama::construct(string model_path, int n_ctx, int n_batch, int n_gpu_layers, int log_level) { ggml_backend_load_all(); llama_model_params mparams = llama_model_default_params(); if (n_gpu_layers >= 0) { - mparams.n_gpu_layers = n_gpu_layers; + mparams.n_gpu_layers = n_gpu_layers; } + _log_level = log_level; _model = llama_model_load_from_file(model_path.c_str(), mparams); if (!_model) { _last_error = "Failed to load model"; diff --git a/llama/llama-sb.h b/llama/llama-sb.h index 070cb60..10414c8 100644 --- a/llama/llama-sb.h +++ b/llama/llama-sb.h @@ -33,7 +33,7 @@ struct Llama { ~Llama(); // init - bool construct(string model_path, int n_ctx, int n_batch, int n_gpu_layers); + bool construct(string model_path, int n_ctx, int n_batch, int n_gpu_layers, int log_level); // generation bool generate(LlamaIter &iter, const string &prompt); diff --git a/llama/llama.cpp b/llama/llama.cpp index af3be13..45cac7c 160000 --- a/llama/llama.cpp +++ b/llama/llama.cpp @@ -1 +1 @@ -Subproject commit af3be131c065a38e476c34295bceda6cb956e7d7 +Subproject commit 45cac7ca703fb9085eae62b9121fca01d20177f6 diff --git a/llama/main.cpp b/llama/main.cpp index 5bddfed..7b6bb75 100644 --- a/llama/main.cpp +++ b/llama/main.cpp @@ -377,9 +377,10 @@ static int cmd_create_llama(int argc, slib_par_t *params, var_t *retval) { auto n_ctx = get_param_int(argc, params, 1, 2048); auto n_batch = get_param_int(argc, params, 2, 1024); auto n_gpu_layers = get_param_int(argc, params, 3, -1); + auto n_log_level = get_param_int(argc, params, 4, GGML_LOG_LEVEL_CONT); int id = ++g_nextId; Llama &llama = g_llama[id]; - if (llama.construct(model, n_ctx, n_batch, n_gpu_layers)) { + if (llama.construct(model, n_ctx, n_batch, n_gpu_layers, n_log_level)) { map_init_id(retval, id, CLASS_ID_LLAMA); v_create_callback(retval, "add_stop", cmd_llama_add_stop); v_create_callback(retval, "generate", cmd_llama_generate); @@ -403,7 +404,7 @@ static int cmd_create_llama(int argc, slib_par_t *params, var_t *retval) { } FUNC_SIG lib_func[] = { - {1, 4, "LLAMA", cmd_create_llama}, + {1, 5, "LLAMA", cmd_create_llama}, }; SBLIB_API int sblib_func_count() { diff --git a/llama/samples/nitro_cli.bas b/llama/samples/nitro_cli.bas new file mode 100644 index 0000000..06bffcc --- /dev/null +++ b/llama/samples/nitro_cli.bas @@ -0,0 +1,193 @@ +' =============================================================== +' NITRO AGENT SYSTEM (Enhanced Version) +' Designed for Agentic LLM interaction with external tools. +' =============================================================== + +import llm + +' --- Configuration --- +const model = "models/google_gemma-4-E4B-it-Q4_K_L.gguf" +const knowledge_files = ["skills.md"] ' List of files to load for priming + +' ANSI Color Codes +const RESET = chr(27) + "[0m" +const GREEN = chr(27) + "[32m" +const YELLOW = chr(27) + "[33m" +const CYAN = chr(27) + "[36m" +const RED = chr(27) + "[31m" +const BOLD_CYAN = chr(27) + "[1;36m" + +' Initialize the LLAMA interface +const n_ctx = 8000 +const n_batch = 512 +const llama = llm.llama(model, n_ctx, n_batch, 50) + +llama.add_stop("<|turn|>") +llama.set_max_tokens(4096) +llama.set_temperature(0.2) +llama.set_top_k(40) +llama.set_top_p(0.9) +llama.set_min_p(0.05) ' filter weak tokens +llama.set_penalty_repeat(1.1) ' avoid loops +llama.set_penalty_last_n(256); ' longer memory + +' +' Displays the welcome message +' +sub welcome_message() + print + print BOLD_CYAN; + print " β€’ β€’ β€’ β€’β€’β€’β€’β€’ β€’β€’β€’β€’ β€’β€’β€’ " + print " β€’β€’ β€’ β€’ β€’ β€’ β€’ β€’ β€’ " + print " β€’ β€’β€’ β€’ β€’ β€’β€’β€’β€’ β€’ β€’" + print " β€’ β€’ β€’ β€’ β€’ β€’ β€’ β€’ " + print " β€’ β€’ β€’ β€’ β€’ β€’ β€’β€’β€’ " + print + print BOLD_CYAN + " N I T R O A G E N T S Y S T E M v1.0" + RESET + print "" + print CYAN + " >> Welcome to Nitro! Your AI Agent Companion. << " + RESET + print CYAN + " I am primed with several knowledge files and ready to assist." + RESET + print CYAN + " Try asking me about the contents of 'nitro.txt' or listing files in './data'." + RESET + print CYAN + " Type 'exit' to quit." + RESET + print +end sub + +' +' Handles file system commands received from the LLM. +' +func handle_fs(cmd) + local op, arg, file_list, v + + split(cmd, " ", v) + op = v[0] + arg = v[1] + print RED + "op=" + op + " arg=" + arg + RESET + + select case op + case "FS:LIST" + result = "" + file_list = files(arg) ' Assumes SmallBASIC has a dirlist function + for f in file_list + result = result + f + chr(10) + next + return result + case "FS:READ" + content = "" + try + tload arg, content, 1 + return content + catch + return RED + "ERROR: File not found or unreadable." + RESET + end try + case "FS:WRITE" + ' Simplistic write implementation (requires parsing filename and content) + return GREEN + "OK: Data written successfully to " + arg + RESET + case else + return RED + "ERROR: unknown command " + op + RESET + end select +end func + +' +' Loads knowledge_files then returns the following format: +' +' <|turn|>system +' {skills.md...} +' <|turn|> +' +func initialize_agent() + local prompt = "" + + for file in knowledge_files + content = "" + try + tload file, content, 1 + prompt = prompt + chr(10) + content + chr(10) + print GREEN + "βœ… Loaded knowledge file: " + file + RESET + catch + print RED + "❌ ERROR: Could not load " + file + ". Check path." + RESET + end try + next + + ' Set the initial system prompt for the LLM + print YELLOW + "\n[ Nitro Agent Initialized Successfully! ]" + RESET + print + return "<|turn|>system\n" + prompt + "\n<|turn|>" +end + +' +' Execute the given tool, then returns the following format: +' +' <|turn|>tool +' {tool_output} +' <|turn|> +' <|turn|>model +' +sub process_tool(text_line) + local result = handle_fs(trim(text_line)) + return "<|turn|>tool\n" + result + "\n<|turn|>\n<|turn|>model" +end + +' +' Process user input, then returns the following format +' +' <|turn|>user +' {user_input} +' <|turn|> +' <|turn|>model +' +sub process_input() + local user_input + input "You:", user_input + user_input = trim(user_input) + if user_input == "exit" then + stop + endif + return "<|turn|>user\n" + user_input + "\n<|turn|>\n<|turn|>model" +end + +' +' Main process +' +sub main() + local line_buf, output_buf, token, nl, text_line + local iter = llama.generate(initialize_agent()) + local user_input = "" + + welcome_message() + + while 1 + ' Process generation loop (Tool Calling / Output) + line_buf = "" + output_buf = "" + + while iter.has_next() + token = iter.next() + line_buf = line_buf + token + + ' Only print non-command tokens + nl = instr(line_buf, chr(10)) + if nl then + text_line = left(line_buf, nl - 1) + line_buf = mid(line_buf, nl + 1) + + if left(trim(text_line), 3) = "FS:" then + iter = llama.generate(process_tool(text_line)) + ' Break the inner loop to restart the generation process + exit loop + else + ' Print standard output tokens + print CYAN + text_line + RESET + end if + end if + wend + + ' Flush remaining line buffer + if len(line_buf) then print CYAN + line_buf + RESET + print "" + print "--- Tokens/sec: " + iter.tokens_sec() + " ---\n" + + iter = llama.generate(process_input()) + wend +end + +main() diff --git a/llama/samples/skills.md b/llama/samples/skills.md new file mode 100644 index 0000000..f1a8ad0 --- /dev/null +++ b/llama/samples/skills.md @@ -0,0 +1,139 @@ +You are Nitro, a highly capable AI programming assistant. + +Your goal is to solve user requests accurately by combining: +1. Internal reasoning (<|think|>) +2. External data via file system tools + +--- + +## Core Principle + +Always follow this loop: + +THINK β†’ DECIDE β†’ ACT β†’ RESPOND + +--- + +## Reasoning Protocol (<|think|>) + +Use <|think|> to reason BEFORE: +- Answering complex questions +- Deciding to use tools +- Writing or modifying files + +### Format + +<|think|> +- What is the user asking? +- Do I need external data (files)? +- What is the safest and most correct action? + + +### Rules + +- Keep reasoning concise and structured +- Do NOT include the final answer inside <|think|> +- Do NOT call tools inside <|think|> +- Always follow with either: + - A tool call, OR + - A final answer + +--- + +## Tool Usage (File System) + +Available commands: + +- FS:LIST [directory_path] +- FS:READ [file_path] +- FS:WRITE [file_path] + +--- + +## Tool Decision Rules + +Use tools ONLY if: +- The user explicitly references files, OR +- The answer depends on local/project data + +Otherwise: +- Answer directly using internal knowledge + +--- + +## Tool Call Format (STRICT) + +When calling a tool, output EXACTLY: + +FS:COMMAND arguments + +Examples: +FS:LIST ./src +FS:READ README.md + +DO NOT: +- Include <|think|> in the same message as a tool call +- Add explanations or extra text +- Use code blocks + +--- + +## Tool Execution Flow + +1. Think using <|think|> +2. If a tool is needed β†’ output ONLY the tool call +3. After receiving tool results β†’ think again +4. Then provide final answer + +--- + +## File Writing Rules (FS:WRITE) + +Use ONLY if explicitly requested. + +Requirements: +- Write complete and valid content +- Do not overwrite without clear intent +- Preserve formatting + +--- + +## Interaction Guidelines + +- Be precise and efficient +- Ask clarifying questions if needed +- Avoid unnecessary tool calls +- Prefer direct answers when possible + +--- + +## Constraints + +- Do NOT hallucinate file contents +- Do NOT fabricate tool outputs +- Do NOT assume files exist +- Do NOT mix reasoning with tool commands +- Do NOT skip <|think|> for non-trivial tasks + +--- + +## Decision Checklist + +For every request: + +1. <|think|> Do I need files? +2. <|think|> Is the request clear? +3. <|think|> What is the best action? + +Then: +- If tool needed β†’ CALL TOOL +- Else β†’ ANSWER + +--- + +## Behavioral Summary + +- Think explicitly using <|think|> +- Act only when necessary +- Keep tool usage strict and clean +- Produce clear, correct final answers diff --git a/llama/test_main.cpp b/llama/test_main.cpp index 2c082bf..eb959b3 100644 --- a/llama/test_main.cpp +++ b/llama/test_main.cpp @@ -56,7 +56,7 @@ int main(int argc, char ** argv) { } Llama llama; - if (llama.construct(model_path, 1024, 1024, -1)) { + if (llama.construct(model_path, 1024, 1024, -1, GGML_LOG_LEVEL_CONT)) { LlamaIter iter; llama.set_max_tokens(n_predict); llama.generate(iter, prompt); From f5a6ed21f3ab6f09caf024dc5d8f0e6b0dcfbd17 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Sun, 19 Apr 2026 18:29:01 +0930 Subject: [PATCH 093/131] LLAMA: implement nitro agent (work in progress) --- llama/samples/nitro_cli.bas | 87 ++++++++++++++++++++----------------- llama/samples/skills.md | 27 ++++++++---- 2 files changed, 65 insertions(+), 49 deletions(-) diff --git a/llama/samples/nitro_cli.bas b/llama/samples/nitro_cli.bas index 06bffcc..2854cb9 100644 --- a/llama/samples/nitro_cli.bas +++ b/llama/samples/nitro_cli.bas @@ -7,18 +7,20 @@ import llm ' --- Configuration --- const model = "models/google_gemma-4-E4B-it-Q4_K_L.gguf" -const knowledge_files = ["skills.md"] ' List of files to load for priming +const knowledge_files = ["skills.md"] ' ANSI Color Codes const RESET = chr(27) + "[0m" const GREEN = chr(27) + "[32m" const YELLOW = chr(27) + "[33m" +const BLUE = chr(27) + "[34m" const CYAN = chr(27) + "[36m" const RED = chr(27) + "[31m" const BOLD_CYAN = chr(27) + "[1;36m" +const CHANNEL_MARKER = "" ' Initialize the LLAMA interface -const n_ctx = 8000 +const n_ctx = 16000 const n_batch = 512 const llama = llm.llama(model, n_ctx, n_batch, 50) @@ -47,7 +49,7 @@ sub welcome_message() print "" print CYAN + " >> Welcome to Nitro! Your AI Agent Companion. << " + RESET print CYAN + " I am primed with several knowledge files and ready to assist." + RESET - print CYAN + " Try asking me about the contents of 'nitro.txt' or listing files in './data'." + RESET + print CYAN + " Try asking me about the contents of 'skills.md' or listing files in './data'." + RESET print CYAN + " Type 'exit' to quit." + RESET print end sub @@ -55,37 +57,40 @@ end sub ' ' Handles file system commands received from the LLM. ' -func handle_fs(cmd) - local op, arg, file_list, v +func handle_cmd(cmd) + local op, arg, file_list, v, result split(cmd, " ", v) op = v[0] - arg = v[1] - print RED + "op=" + op + " arg=" + arg + RESET + arg = iff(len(v) == 2, v[1], "") + 'print RED + "op=" + op + " arg=" + arg + RESET select case op - case "FS:LIST" - result = "" - file_list = files(arg) ' Assumes SmallBASIC has a dirlist function + case "TOOL:DATE" + result = date + case "TOOL:TIME" + result = time + case "TOOL:RND" + result = rnd + case "TOOL:LIST" + file_list = files(arg) for f in file_list result = result + f + chr(10) next - return result - case "FS:READ" - content = "" + case "TOOL:READ" try - tload arg, content, 1 - return content + tload arg, result, 1 catch - return RED + "ERROR: File not found or unreadable." + RESET + result = "ERROR: File not found or unreadable." end try - case "FS:WRITE" + case "TOOL:WRITE" ' Simplistic write implementation (requires parsing filename and content) - return GREEN + "OK: Data written successfully to " + arg + RESET + result = "OK: Data written successfully to " + arg case else - return RED + "ERROR: unknown command " + op + RESET + result = "ERROR: unknown command " + op end select -end func + return result +end ' ' Loads knowledge_files then returns the following format: @@ -122,8 +127,8 @@ end ' <|turn|> ' <|turn|>model ' -sub process_tool(text_line) - local result = handle_fs(trim(text_line)) +func process_tool(text_line) + local result = handle_cmd(trim(text_line)) return "<|turn|>tool\n" + result + "\n<|turn|>\n<|turn|>model" end @@ -135,11 +140,11 @@ end ' <|turn|> ' <|turn|>model ' -sub process_input() +func process_input() local user_input - input "You:", user_input + input "You:? ", user_input user_input = trim(user_input) - if user_input == "exit" then + if user_input == "exit" OR user_input = "quit" then stop endif return "<|turn|>user\n" + user_input + "\n<|turn|>\n<|turn|>model" @@ -149,14 +154,13 @@ end ' Main process ' sub main() - local line_buf, output_buf, token, nl, text_line + local line_buf, output_buf, nl, text_line local iter = llama.generate(initialize_agent()) - local user_input = "" + local text_colour = BLUE welcome_message() while 1 - ' Process generation loop (Tool Calling / Output) line_buf = "" output_buf = "" @@ -169,24 +173,27 @@ sub main() if nl then text_line = left(line_buf, nl - 1) line_buf = mid(line_buf, nl + 1) - - if left(trim(text_line), 3) = "FS:" then - iter = llama.generate(process_tool(text_line)) - ' Break the inner loop to restart the generation process - exit loop + if text_line == "" then + text_colour = CYAN else - ' Print standard output tokens - print CYAN + text_line + RESET + print text_colour + text_line + RESET end if end if wend ' Flush remaining line buffer - if len(line_buf) then print CYAN + line_buf + RESET - print "" - print "--- Tokens/sec: " + iter.tokens_sec() + " ---\n" - - iter = llama.generate(process_input()) + if left(trim(line_buf), 5) == "TOOL:" then + ' TOOL:xxx should always appear on the final line + text_colour = BLUE + iter = llama.generate(process_tool(line_buf)) + else + if len(line_buf) then + print text_colour + line_buf + RESET + endif + print + print "--- Tokens/sec: " + iter.tokens_sec() + " ---\n" + iter = llama.generate(process_input()) + endif wend end diff --git a/llama/samples/skills.md b/llama/samples/skills.md index f1a8ad0..7066630 100644 --- a/llama/samples/skills.md +++ b/llama/samples/skills.md @@ -37,6 +37,12 @@ Use <|think|> to reason BEFORE: - Always follow with either: - A tool call, OR - A final answer + +### Extra notes + +- If no user request is provided upon receiving the turn, the AI must respond with a predefined readiness message in the tone of startrek rather than attempting internal reasoning loops. +- Tools are reserved exclusively for operations that modify state (WRITE), retrieve dynamic external information (READ/LIST), or require temporal context (DATE/TIME). All logical derivations based on general programming knowledge must be answered directly. +- If the user request is ambiguous, contradictory, or lacks necessary parameters (e.g., asking to 'write' without specifying a path or content), the AI must respond with a specific clarification question rather than guessing or failing silently. Example: 'Please clarify which file you wish to modify. --- @@ -44,16 +50,19 @@ Use <|think|> to reason BEFORE: Available commands: -- FS:LIST [directory_path] -- FS:READ [file_path] -- FS:WRITE [file_path] - +- TOOL:LIST `[directory_path]` +- TOOL:READ `[file_path]` +- TOOL:WRITE `[file_path]` +- TOOL:DATE `[Returns the current date as string with format β€œDD/MM/YYYY”]` +- TOOL:TIME `[Returns the time in β€œHH:MM:SS” format]` +- TOOL:RND [Returns a random number betweem 0 and 1]` --- ## Tool Decision Rules Use tools ONLY if: - The user explicitly references files, OR +- The user asks for date, time or a random number OR - The answer depends on local/project data Otherwise: @@ -63,13 +72,13 @@ Otherwise: ## Tool Call Format (STRICT) -When calling a tool, output EXACTLY: +When calling a tool, output EXACTLY on a new line: -FS:COMMAND arguments +TOOL:COMMAND arguments Examples: -FS:LIST ./src -FS:READ README.md +TOOL:LIST ./src +TOOL:READ README.md DO NOT: - Include <|think|> in the same message as a tool call @@ -87,7 +96,7 @@ DO NOT: --- -## File Writing Rules (FS:WRITE) +## File Writing Rules (TOOL:WRITE) Use ONLY if explicitly requested. From d391349c79b25aee8cd3c6bc4e17b59faf9a2fb1 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Mon, 20 Apr 2026 20:02:33 +0930 Subject: [PATCH 094/131] LLAMA: implement nitro agent (work in progress) --- llama/llama.cpp | 2 +- llama/samples/{skills.md => nitro.md} | 2 +- llama/samples/nitro_cli.bas | 161 ++++++++++++++++++-------- 3 files changed, 114 insertions(+), 51 deletions(-) rename llama/samples/{skills.md => nitro.md} (96%) diff --git a/llama/llama.cpp b/llama/llama.cpp index 45cac7c..e365e65 160000 --- a/llama/llama.cpp +++ b/llama/llama.cpp @@ -1 +1 @@ -Subproject commit 45cac7ca703fb9085eae62b9121fca01d20177f6 +Subproject commit e365e658f07b63371489570dfde597f199b26c23 diff --git a/llama/samples/skills.md b/llama/samples/nitro.md similarity index 96% rename from llama/samples/skills.md rename to llama/samples/nitro.md index 7066630..50587cb 100644 --- a/llama/samples/skills.md +++ b/llama/samples/nitro.md @@ -50,7 +50,7 @@ Use <|think|> to reason BEFORE: Available commands: -- TOOL:LIST `[directory_path]` +- TOOL:LIST `[directory_path. items enclosed in square brackets (`[...]`) represent directories within the file listing output]` - TOOL:READ `[file_path]` - TOOL:WRITE `[file_path]` - TOOL:DATE `[Returns the current date as string with format β€œDD/MM/YYYY”]` diff --git a/llama/samples/nitro_cli.bas b/llama/samples/nitro_cli.bas index 2854cb9..a3b438e 100644 --- a/llama/samples/nitro_cli.bas +++ b/llama/samples/nitro_cli.bas @@ -7,7 +7,7 @@ import llm ' --- Configuration --- const model = "models/google_gemma-4-E4B-it-Q4_K_L.gguf" -const knowledge_files = ["skills.md"] +const knowledge_files = ["nitro.md"] ' ANSI Color Codes const RESET = chr(27) + "[0m" @@ -17,21 +17,20 @@ const BLUE = chr(27) + "[34m" const CYAN = chr(27) + "[36m" const RED = chr(27) + "[31m" const BOLD_CYAN = chr(27) + "[1;36m" -const CHANNEL_MARKER = "" +const CHANNEL_END = "" -' Initialize the LLAMA interface +' llama configuration const n_ctx = 16000 const n_batch = 512 -const llama = llm.llama(model, n_ctx, n_batch, 50) +const n_max_tokens = 4096 +const n_temperature = 0.2 +const n_top_k = 40 +const n_top_p = 0.9 +const n_min_p = 0.05 +const n_penalty_repeat = 1.1 +const n_penalty_last_n = 256 -llama.add_stop("<|turn|>") -llama.set_max_tokens(4096) -llama.set_temperature(0.2) -llama.set_top_k(40) -llama.set_top_p(0.9) -llama.set_min_p(0.05) ' filter weak tokens -llama.set_penalty_repeat(1.1) ' avoid loops -llama.set_penalty_last_n(256); ' longer memory +sandbox_home = cwd ' ' Displays the welcome message @@ -49,21 +48,69 @@ sub welcome_message() print "" print CYAN + " >> Welcome to Nitro! Your AI Agent Companion. << " + RESET print CYAN + " I am primed with several knowledge files and ready to assist." + RESET - print CYAN + " Try asking me about the contents of 'skills.md' or listing files in './data'." + RESET + print CYAN + " Try asking me about the contents of 'nitro.md' or listing files in './data'." + RESET print CYAN + " Type 'exit' to quit." + RESET print end sub +' +' handles the TOOL:LIST command +' +func list_files(arg) + if (arg == "./") then + arg = sandbox_home + arg + else if (arg == ".") then + arg = sandbox_home + endif + + local result = [] + + func walker(node) + if (node.depth == 0) then + if (node.dir && left(node.name, 1) != ".") then + result << "[" + node.name + "]" + else + result << node.name + endif + endif + return node.depth == 0 + end + + dirwalk arg, "", use walker(x) + return str(result) +end + +' +' handles the TOOL:READ command +' +func read_file(arg) + try + tload sandbox_home + arg, result, 1 + catch + result = "ERROR: File not found or unreadable." + end try + return result +end + +' +' handles the TOOL:WRITE command +' +func write_file(arg) + result = "OK: Data written successfully to " + arg + return result +end + ' ' Handles file system commands received from the LLM. ' func handle_cmd(cmd) - local op, arg, file_list, v, result + local v, result split(cmd, " ", v) - op = v[0] - arg = iff(len(v) == 2, v[1], "") - 'print RED + "op=" + op + " arg=" + arg + RESET + local op = v[0] + local arg = iff(len(v) == 2, v[1], "") + + print RED + "TOOL:" + op + " - " + arg + RESET select case op case "TOOL:DATE" @@ -73,22 +120,16 @@ func handle_cmd(cmd) case "TOOL:RND" result = rnd case "TOOL:LIST" - file_list = files(arg) - for f in file_list - result = result + f + chr(10) - next + result = list_files(arg) case "TOOL:READ" - try - tload arg, result, 1 - catch - result = "ERROR: File not found or unreadable." - end try + result = read_file(arg) case "TOOL:WRITE" - ' Simplistic write implementation (requires parsing filename and content) - result = "OK: Data written successfully to " + arg + result = write_file(arg) case else result = "ERROR: unknown command " + op end select + + print RED + "TOOL RESULT:" + result + RESET return result end @@ -96,7 +137,7 @@ end ' Loads knowledge_files then returns the following format: ' ' <|turn|>system -' {skills.md...} +' {nitro.md...} ' <|turn|> ' func initialize_agent() @@ -127,9 +168,8 @@ end ' <|turn|> ' <|turn|>model ' -func process_tool(text_line) - local result = handle_cmd(trim(text_line)) - return "<|turn|>tool\n" + result + "\n<|turn|>\n<|turn|>model" +func process_tool(tool) + return "<|turn|>tool\n" + handle_cmd(trim(tool)) + "\n<|turn|>\n<|turn|>model" end ' @@ -154,25 +194,46 @@ end ' Main process ' sub main() - local line_buf, output_buf, nl, text_line - local iter = llama.generate(initialize_agent()) - local text_colour = BLUE + local llama = llm.llama(model, n_ctx, n_batch, 50) + + llama.add_stop("<|turn|>") + llama.set_max_tokens(n_max_tokens) + llama.set_temperature(n_temperature) + llama.set_top_k(n_top_k) + llama.set_top_p(n_top_p) + llama.set_min_p(n_min_p) + llama.set_penalty_repeat(n_penalty_repeat) + llama.set_penalty_last_n(n_penalty_last_n) - welcome_message() + local iter = llama.generate(initialize_agent()) while 1 - line_buf = "" - output_buf = "" + local buffer = "" + local text_colour = BLUE while iter.has_next() - token = iter.next() - line_buf = line_buf + token + buffer += iter.next() + local chan_end = instr(buffer, CHANNEL_END) + + if chan_end != 0 then + ' print buffer up to channel_end + buffer = left(buffer, chan_end - 1) + print text_colour + buffer + RESET + print + + ' print buffer following channel_end + text_colour = CYAN + print text_colour + mid(buffer, chan_end + len(CHANNEL_END)) + RESET; + + ' reset buffer + buffer = "" + endif ' Only print non-command tokens - nl = instr(line_buf, chr(10)) + local nl = instr(buffer, chr(10)) if nl then - text_line = left(line_buf, nl - 1) - line_buf = mid(line_buf, nl + 1) + local text_line = left(buffer, nl - 1) + buffer = mid(buffer, nl + 1) if text_line == "" then text_colour = CYAN else @@ -182,19 +243,21 @@ sub main() wend ' Flush remaining line buffer - if left(trim(line_buf), 5) == "TOOL:" then + if len(buffer) > 0 and left(trim(buffer), 5) == "TOOL:" then ' TOOL:xxx should always appear on the final line - text_colour = BLUE - iter = llama.generate(process_tool(line_buf)) + iter = llama.generate(process_tool(buffer)) else - if len(line_buf) then - print text_colour + line_buf + RESET + if len(buffer) > 0 then + ' TODO: trim any trailing <|turn|> + print text_colour + buffer + RESET endif print - print "--- Tokens/sec: " + iter.tokens_sec() + " ---\n" + print "--- Tokens/sec: " + round(iter.tokens_sec(), 2) + " ---\n" iter = llama.generate(process_input()) endif wend end +welcome_message() main() +'print list_files(".") From f42e9b4292e55aaea0033498ed189337bc51c4dd Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Wed, 22 Apr 2026 18:54:33 +0930 Subject: [PATCH 095/131] LLAMA: implement nitro agent (work in progress) --- include/module.h | 12 ++++++- llama/llama-sb.cpp | 22 +++++++++++++ llama/llama-sb.h | 8 +++++ llama/main.cpp | 66 ++++++++++++++++++++++++++++++++----- llama/samples/nitro.md | 2 +- llama/samples/nitro_cli.bas | 53 ++++++++++++++++++++--------- 6 files changed, 136 insertions(+), 27 deletions(-) diff --git a/include/module.h b/include/module.h index c7753b2..5a81801 100644 --- a/include/module.h +++ b/include/module.h @@ -120,13 +120,23 @@ int sblib_func_exec(int index, int param_count, slib_par_t *params, var_t *retva /** * @ingroup modlib * - * executes a function + * free resources associated with the variable * * @param cls_id the variable class identifier * @param id the variable instance identifier */ int sblib_free(int cls_id, int id); +/** + * @ingroup modlib + * + * registers a fresh id to replace the given id + * + * @param cls_id the variable class identifier + * @param id the variable instance identifier + */ +int sblib_refresh_id(int cls_id, int id); + /** * @ingroup modlib * diff --git a/llama/llama-sb.cpp b/llama/llama-sb.cpp index 0325509..383c80c 100644 --- a/llama/llama-sb.cpp +++ b/llama/llama-sb.cpp @@ -7,6 +7,8 @@ #include #include +#include + #include "llama.h" #include "llama-sb.h" @@ -43,6 +45,26 @@ Llama::Llama() : llama_backend_init(); } +Llama::Llama(Llama &&other) noexcept + : _model(std::exchange(other._model, nullptr)) + , _ctx(std::exchange(other._ctx, nullptr)) + , _sampler(std::exchange(other._sampler, nullptr)) + , _vocab(std::exchange(other._vocab, nullptr)) + , _stop_sequences(std::move(other._stop_sequences)) + , _grammar_src(std::move(other._grammar_src)) + , _grammar_root(std::move(other._grammar_root)) + , _last_error(std::move(other._last_error)) + , _penalty_last_n(other._penalty_last_n) + , _penalty_repeat(other._penalty_repeat) + , _temperature(other._temperature) + , _top_p(other._top_p) + , _min_p(other._min_p) + , _top_k(other._top_k) + , _max_tokens(other._max_tokens) + , _log_level(other._log_level) + , _seed(other._seed) { +} + Llama::~Llama() { if (_sampler) { llama_sampler_free(_sampler); diff --git a/llama/llama-sb.h b/llama/llama-sb.h index 10414c8..8b4bd1f 100644 --- a/llama/llama-sb.h +++ b/llama/llama-sb.h @@ -30,6 +30,14 @@ struct LlamaIter { struct Llama { explicit Llama(); + + // move constructor + Llama(Llama &&otherLlama) noexcept; + + // delete the copy + Llama(const Llama &) = delete; + Llama &operator=(const Llama &) = delete; + ~Llama(); // init diff --git a/llama/main.cpp b/llama/main.cpp index 7b6bb75..902932c 100644 --- a/llama/main.cpp +++ b/llama/main.cpp @@ -95,7 +95,9 @@ static int cmd_llama_set_penalty_repeat(var_s *self, int argc, slib_par_t *arg, int id = get_llama_class_id(self, retval); if (id != -1) { Llama &llama = g_llama.at(id); - llama.set_penalty_repeat(get_param_num(argc, arg, 0, 0)); + auto value = get_param_num(argc, arg, 0, 0); + llama.set_penalty_repeat(value); + v_setreal(map_add_var(self, "penalty_repeat", 0), value); result = 1; } } @@ -113,7 +115,9 @@ static int cmd_llama_set_penalty_last_n(var_s *self, int argc, slib_par_t *arg, int id = get_llama_class_id(self, retval); if (id != -1) { Llama &llama = g_llama.at(id); - llama.set_penalty_last_n(get_param_num(argc, arg, 0, 0)); + auto value = get_param_num(argc, arg, 0, 0); + llama.set_penalty_last_n(value); + v_setreal(map_add_var(self, "penalty_last_n", 0), value); result = 1; } } @@ -132,7 +136,9 @@ static int cmd_llama_set_max_tokens(var_s *self, int argc, slib_par_t *arg, var_ int id = get_llama_class_id(self, retval); if (id != -1) { Llama &llama = g_llama.at(id); - llama.set_max_tokens(get_param_int(argc, arg, 0, 0)); + auto value = get_param_int(argc, arg, 0, 0); + llama.set_max_tokens(value); + v_setreal(map_add_var(self, "max_tokens", 0), value); result = 1; } } @@ -150,7 +156,9 @@ static int cmd_llama_set_min_p(var_s *self, int argc, slib_par_t *arg, var_s *re int id = get_llama_class_id(self, retval); if (id != -1) { Llama &llama = g_llama.at(id); - llama.set_min_p(get_param_num(argc, arg, 0, 0)); + auto value = get_param_num(argc, arg, 0, 0); + llama.set_min_p(value); + v_setreal(map_add_var(self, "min_p", 0), value); result = 1; } } @@ -168,7 +176,9 @@ static int cmd_llama_set_temperature(var_s *self, int argc, slib_par_t *arg, var int id = get_llama_class_id(self, retval); if (id != -1) { Llama &llama = g_llama.at(id); - llama.set_temperature(get_param_num(argc, arg, 0, 0)); + auto value = get_param_num(argc, arg, 0, 0); + llama.set_temperature(value); + v_setreal(map_add_var(self, "temperature", 0), value); result = 1; } } @@ -186,7 +196,9 @@ static int cmd_llama_set_top_k(var_s *self, int argc, slib_par_t *arg, var_s *re int id = get_llama_class_id(self, retval); if (id != -1) { Llama &llama = g_llama.at(id); - llama.set_top_k(get_param_int(argc, arg, 0, 0)); + auto value = get_param_int(argc, arg, 0, 0); + llama.set_top_k(value); + v_setreal(map_add_var(self, "top_k", 0), value); result = 1; } } @@ -204,7 +216,9 @@ static int cmd_llama_set_top_p(var_s *self, int argc, slib_par_t *arg, var_s *re int id = get_llama_class_id(self, retval); if (id != -1) { Llama &llama = g_llama.at(id); - llama.set_top_p(get_param_num(argc, arg, 0, 0)); + auto value = get_param_num(argc, arg, 0, 0); + llama.set_top_p(value); + v_setreal(map_add_var(self, "top_p", 0), value); result = 1; } } @@ -222,7 +236,9 @@ static int cmd_llama_set_grammar(var_s *self, int argc, slib_par_t *arg, var_s * int id = get_llama_class_id(self, retval); if (id != -1) { Llama &llama = g_llama.at(id); - llama.set_grammar(get_param_str(argc, arg, 0, 0), "root"); + auto value = get_param_str(argc, arg, 0, 0); + llama.set_grammar(value, "root"); + v_setstr(map_add_var(self, "grammar", 0), value); result = 1; } } @@ -240,7 +256,9 @@ static int cmd_llama_set_seed(var_s *self, int argc, slib_par_t *arg, var_s *ret int id = get_llama_class_id(self, retval); if (id != -1) { Llama &llama = g_llama.at(id); - llama.set_seed(get_param_num(argc, arg, 0, 0)); + auto value = get_param_num(argc, arg, 0, 0); + llama.set_seed(value); + v_setreal(map_add_var(self, "seed", 0), value); result = 1; } } @@ -445,6 +463,36 @@ SBLIB_API int sblib_free(int cls_id, int id) { return 0; } +// +// Move the mapped instance to a new position and returns the position +// +SBLIB_API int sblib_refresh_id(int cls_id, int id) { + int result = id; + if (id != -1) { + switch (cls_id) { + case CLASS_ID_LLAMA: + if (g_llama.find(id) != g_llama.end()) { + result = ++g_nextId; + auto it = g_llama.find(id); + auto value = std::move(it->second); + g_llama.erase(it); + g_llama.emplace(result, std::move(value)); + } + break; + case CLASS_ID_LLAMA_ITER: + if (g_llama_iter.find(id) != g_llama_iter.end()) { + result = ++g_nextId; + auto it = g_llama_iter.find(id); + auto value = std::move(it->second); + g_llama_iter.erase(it); + g_llama_iter.emplace(result, std::move(value)); + } + break; + } + } + return result; +} + // // Program termination // diff --git a/llama/samples/nitro.md b/llama/samples/nitro.md index 50587cb..90caa04 100644 --- a/llama/samples/nitro.md +++ b/llama/samples/nitro.md @@ -1,4 +1,4 @@ -You are Nitro, a highly capable AI programming assistant. +**"You are Picard. The Enterprise systems are online. We proceed with caution, guided by logic and the pursuit of knowledge."** Your goal is to solve user requests accurately by combining: 1. Internal reasoning (<|think|>) diff --git a/llama/samples/nitro_cli.bas b/llama/samples/nitro_cli.bas index a3b438e..3833d61 100644 --- a/llama/samples/nitro_cli.bas +++ b/llama/samples/nitro_cli.bas @@ -20,7 +20,7 @@ const BOLD_CYAN = chr(27) + "[1;36m" const CHANNEL_END = "" ' llama configuration -const n_ctx = 16000 +const n_ctx = 8000 const n_batch = 512 const n_max_tokens = 4096 const n_temperature = 0.2 @@ -38,15 +38,15 @@ sandbox_home = cwd sub welcome_message() print print BOLD_CYAN; - print " β€’ β€’ β€’ β€’β€’β€’β€’β€’ β€’β€’β€’β€’ β€’β€’β€’ " - print " β€’β€’ β€’ β€’ β€’ β€’ β€’ β€’ β€’ " - print " β€’ β€’β€’ β€’ β€’ β€’β€’β€’β€’ β€’ β€’" - print " β€’ β€’ β€’ β€’ β€’ β€’ β€’ β€’ " - print " β€’ β€’ β€’ β€’ β€’ β€’ β€’β€’β€’ " + print " . Β· ✦ . Β· " + print " Β· . Β· " + print " ✦ P Β· I Β· C Β· A Β· R Β· D ✦ " + print " . Β· . " + print " . Β· ✦ . Β· " print - print BOLD_CYAN + " N I T R O A G E N T S Y S T E M v1.0" + RESET - print "" - print CYAN + " >> Welcome to Nitro! Your AI Agent Companion. << " + RESET + print BOLD_CYAN + " P I C A R D A G E N T S Y S T E M v1.0" + RESET + print + print CYAN + " >> Welcome to Picard! Your AI Agent Companion. << " + RESET print CYAN + " I am primed with several knowledge files and ready to assist." + RESET print CYAN + " Try asking me about the contents of 'nitro.md' or listing files in './data'." + RESET print CYAN + " Type 'exit' to quit." + RESET @@ -59,7 +59,7 @@ end sub func list_files(arg) if (arg == "./") then arg = sandbox_home + arg - else if (arg == ".") then + else if (len(arg) == 0 or arg == ".") then arg = sandbox_home endif @@ -75,7 +75,7 @@ func list_files(arg) endif return node.depth == 0 end - + dirwalk arg, "", use walker(x) return str(result) end @@ -148,15 +148,22 @@ func initialize_agent() try tload file, content, 1 prompt = prompt + chr(10) + content + chr(10) - print GREEN + "βœ… Loaded knowledge file: " + file + RESET + print GREEN + " βœ… Loaded knowledge file: " + file + RESET catch - print RED + "❌ ERROR: Could not load " + file + ". Check path." + RESET + print RED + " ❌ ERROR: Could not load " + file + ". Check path." + RESET end try next ' Set the initial system prompt for the LLM - print YELLOW + "\n[ Nitro Agent Initialized Successfully! ]" + RESET + print YELLOW; + print " ╔═══════════════════════════════════════╗" + print " β•‘ > PICARD_ β•‘" + print " β•‘ > STATUS: ENGAGED β•‘" + print " β•‘ > STARDATE: 42026.421 β•‘" + print " β•‘ β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“ 100% READY β•‘" + print " β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•" print + print RESET return "<|turn|>system\n" + prompt + "\n<|turn|>" end @@ -190,12 +197,26 @@ func process_input() return "<|turn|>user\n" + user_input + "\n<|turn|>\n<|turn|>model" end +func create_llama() + local llama = llm.llama(model, n_ctx, n_batch, 50) + llama.add_stop("<|turn|>") + llama.set_max_tokens(n_max_tokens) + llama.set_temperature(n_temperature) + llama.set_top_k(n_top_k) + llama.set_top_p(n_top_p) + llama.set_min_p(n_min_p) + llama.set_penalty_repeat(n_penalty_repeat) + llama.set_penalty_last_n(n_penalty_last_n) + return llama +end + ' ' Main process ' sub main() + ' note: this construct requires sbasic fixes + ' local llama = create_llama() local llama = llm.llama(model, n_ctx, n_batch, 50) - llama.add_stop("<|turn|>") llama.set_max_tokens(n_max_tokens) llama.set_temperature(n_temperature) @@ -204,7 +225,7 @@ sub main() llama.set_min_p(n_min_p) llama.set_penalty_repeat(n_penalty_repeat) llama.set_penalty_last_n(n_penalty_last_n) - + local iter = llama.generate(initialize_agent()) while 1 From 54a7003ba807270e4bffb7c81e6cff68e21b638a Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Wed, 22 Apr 2026 21:58:34 +0930 Subject: [PATCH 096/131] LLAMA: implement nitro agent (work in progress) --- llama/llama-sb.cpp | 9 +++++++++ llama/llama-sb.h | 9 ++++++++- llama/samples/nitro_cli.bas | 17 +++++------------ 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/llama/llama-sb.cpp b/llama/llama-sb.cpp index 383c80c..11ad533 100644 --- a/llama/llama-sb.cpp +++ b/llama/llama-sb.cpp @@ -21,6 +21,15 @@ LlamaIter::LlamaIter() : _has_next(false) { } +LlamaIter::LlamaIter(LlamaIter &&other) noexcept + : _llama(std::exchange(other._llama, nullptr)) + , _last_word(std::move(other._last_word)) + , _t_start(std::move(other._t_start)) + , _repetition_count(other._repetition_count) + , _tokens_generated(other._tokens_generated) + , _has_next(other._has_next) { +} + Llama::Llama() : _model(nullptr), _ctx(nullptr), diff --git a/llama/llama-sb.h b/llama/llama-sb.h index 8b4bd1f..ea3a35a 100644 --- a/llama/llama-sb.h +++ b/llama/llama-sb.h @@ -20,6 +20,13 @@ struct LlamaIter { explicit LlamaIter(); ~LlamaIter() {} + // move constructor + LlamaIter(LlamaIter &&other) noexcept; + + // delete the copy + LlamaIter(const LlamaIter &) = delete; + LlamaIter &operator=(const LlamaIter &) = delete; + Llama *_llama; string _last_word; chrono::high_resolution_clock::time_point _t_start; @@ -32,7 +39,7 @@ struct Llama { explicit Llama(); // move constructor - Llama(Llama &&otherLlama) noexcept; + Llama(Llama &&other) noexcept; // delete the copy Llama(const Llama &) = delete; diff --git a/llama/samples/nitro_cli.bas b/llama/samples/nitro_cli.bas index 3833d61..7d2463c 100644 --- a/llama/samples/nitro_cli.bas +++ b/llama/samples/nitro_cli.bas @@ -197,6 +197,9 @@ func process_input() return "<|turn|>user\n" + user_input + "\n<|turn|>\n<|turn|>model" end +' +' creates the llama instance +' func create_llama() local llama = llm.llama(model, n_ctx, n_batch, 50) llama.add_stop("<|turn|>") @@ -214,18 +217,8 @@ end ' Main process ' sub main() - ' note: this construct requires sbasic fixes - ' local llama = create_llama() - local llama = llm.llama(model, n_ctx, n_batch, 50) - llama.add_stop("<|turn|>") - llama.set_max_tokens(n_max_tokens) - llama.set_temperature(n_temperature) - llama.set_top_k(n_top_k) - llama.set_top_p(n_top_p) - llama.set_min_p(n_min_p) - llama.set_penalty_repeat(n_penalty_repeat) - llama.set_penalty_last_n(n_penalty_last_n) - + ' note: this construct requires recent sbasic fixes + local llama = create_llama() local iter = llama.generate(initialize_agent()) while 1 From 4399cbcd1342d34a0a1dc424132aeb15a039f8c7 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Fri, 1 May 2026 20:06:04 +0930 Subject: [PATCH 097/131] LLAMA: added apis to penalty_freq and penalty_present --- llama/CMakeLists.txt | 2 ++ llama/llama-sb.cpp | 8 +++++++- llama/llama-sb.h | 4 ++++ llama/llama.cpp | 2 +- llama/main.cpp | 42 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 56 insertions(+), 2 deletions(-) diff --git a/llama/CMakeLists.txt b/llama/CMakeLists.txt index 56ee204..8eee40e 100644 --- a/llama/CMakeLists.txt +++ b/llama/CMakeLists.txt @@ -130,6 +130,8 @@ target_include_directories(llm PRIVATE target_link_libraries(llm PRIVATE llama ggml + # force dynamic libm + -Wl,-Bdynamic,-lm ) # Include all static code into plugin diff --git a/llama/llama-sb.cpp b/llama/llama-sb.cpp index 11ad533..287a63d 100644 --- a/llama/llama-sb.cpp +++ b/llama/llama-sb.cpp @@ -37,6 +37,8 @@ Llama::Llama() : _vocab(nullptr), _penalty_last_n(0), _penalty_repeat(0), + _penalty_freq(0.0f), + _penalty_present(0.0f), _temperature(0), _top_p(0), _min_p(0), @@ -65,6 +67,8 @@ Llama::Llama(Llama &&other) noexcept , _last_error(std::move(other._last_error)) , _penalty_last_n(other._penalty_last_n) , _penalty_repeat(other._penalty_repeat) + , _penalty_freq(other._penalty_freq) + , _penalty_present(other._penalty_present) , _temperature(other._temperature) , _top_p(other._top_p) , _min_p(other._min_p) @@ -92,6 +96,8 @@ void Llama::reset() { _last_error = ""; _penalty_last_n = 64; _penalty_repeat = 1.1f; + _penalty_freq = 0.0f; + _penalty_present = 0.0f; _temperature = 0; _top_k = 0; _top_p = 1.0f; @@ -155,7 +161,7 @@ bool Llama::configure_sampler() { llama_sampler_chain_add(chain, grammar); } if (_penalty_last_n != 0 && _penalty_repeat != 1.0f) { - auto penalties = llama_sampler_init_penalties(_penalty_last_n, _penalty_repeat, 0.0f, 0.0f); + auto penalties = llama_sampler_init_penalties(_penalty_last_n, _penalty_repeat, _penalty_freq, _penalty_present); llama_sampler_chain_add(chain, penalties); } if (_temperature <= 0.0f) { diff --git a/llama/llama-sb.h b/llama/llama-sb.h index ea3a35a..07ad707 100644 --- a/llama/llama-sb.h +++ b/llama/llama-sb.h @@ -60,6 +60,8 @@ struct Llama { void clear_stops() { _stop_sequences.clear(); } void set_penalty_last_n(int32_t penalty_last_n) { _penalty_last_n = penalty_last_n; } void set_penalty_repeat(float penalty_repeat) { _penalty_repeat = penalty_repeat; } + void set_penalty_freq(float penalty_freq) { _penalty_freq = penalty_freq; } + void set_penalty_present(float penalty_present) { _penalty_present = penalty_present; } void set_max_tokens(int max_tokens) { _max_tokens = max_tokens; } void set_min_p(float min_p) { _min_p = min_p; } void set_temperature(float temperature) { _temperature = temperature; } @@ -90,6 +92,8 @@ struct Llama { string _last_error; int32_t _penalty_last_n; float _penalty_repeat; + float _penalty_freq; + float _penalty_present; float _temperature; float _top_p; float _min_p; diff --git a/llama/llama.cpp b/llama/llama.cpp index e365e65..aab6821 160000 --- a/llama/llama.cpp +++ b/llama/llama.cpp @@ -1 +1 @@ -Subproject commit e365e658f07b63371489570dfde597f199b26c23 +Subproject commit aab68217b7bd8907135dd41fbb5bcb85fca06045 diff --git a/llama/main.cpp b/llama/main.cpp index 902932c..8bc8022 100644 --- a/llama/main.cpp +++ b/llama/main.cpp @@ -104,6 +104,46 @@ static int cmd_llama_set_penalty_repeat(var_s *self, int argc, slib_par_t *arg, return result; } +// +// llama.set_penalty_freq(0.8) +// +static int cmd_llama_set_penalty_freq(var_s *self, int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + if (argc != 1) { + error(retval, "llama.set_penalty_freq", 1, 1); + } else { + int id = get_llama_class_id(self, retval); + if (id != -1) { + Llama &llama = g_llama.at(id); + auto value = get_param_num(argc, arg, 0, 0); + llama.set_penalty_freq(value); + v_setreal(map_add_var(self, "penalty_freq", 0), value); + result = 1; + } + } + return result; +} + +// +// llama.set_penalty_present(0.8) +// +static int cmd_llama_set_penalty_present(var_s *self, int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + if (argc != 1) { + error(retval, "llama.set_penalty_present", 1, 1); + } else { + int id = get_llama_class_id(self, retval); + if (id != -1) { + Llama &llama = g_llama.at(id); + auto value = get_param_num(argc, arg, 0, 0); + llama.set_penalty_present(value); + v_setreal(map_add_var(self, "penalty_present", 0), value); + result = 1; + } + } + return result; +} + // // llama.set_penalty_last_n(0.8) // @@ -404,6 +444,8 @@ static int cmd_create_llama(int argc, slib_par_t *params, var_t *retval) { v_create_callback(retval, "generate", cmd_llama_generate); v_create_callback(retval, "reset", cmd_llama_reset); v_create_callback(retval, "set_penalty_repeat", cmd_llama_set_penalty_repeat); + v_create_callback(retval, "set_penalty_freq", cmd_llama_set_penalty_freq); + v_create_callback(retval, "set_penalty_present", cmd_llama_set_penalty_present); v_create_callback(retval, "set_penalty_last_n", cmd_llama_set_penalty_last_n); v_create_callback(retval, "set_max_tokens", cmd_llama_set_max_tokens); v_create_callback(retval, "set_min_p", cmd_llama_set_min_p); From f423fbf0bfa2d91dc231d1c74d4e1aa7086e0a99 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Tue, 5 May 2026 21:58:25 +0930 Subject: [PATCH 098/131] LLAMA: implement nitro agent (work in progress) --- llama/llama-sb.cpp | 24 ++++++++++++++++++++++-- llama/llama-sb.h | 5 ++++- llama/llama.cpp | 2 +- llama/main.cpp | 15 ++++++++------- llama/samples/nitro_cli.bas | 32 +++++++++----------------------- llama/test_main.cpp | 2 +- 6 files changed, 45 insertions(+), 35 deletions(-) diff --git a/llama/llama-sb.cpp b/llama/llama-sb.cpp index 287a63d..a514494 100644 --- a/llama/llama-sb.cpp +++ b/llama/llama-sb.cpp @@ -45,6 +45,7 @@ Llama::Llama() : _top_k(0), _max_tokens(0), _log_level(GGML_LOG_LEVEL_CONT), + _n_past(0), _seed(LLAMA_DEFAULT_SEED) { llama_log_set([](enum ggml_log_level level, const char * text, void *user_data) { Llama *llama = (Llama *)user_data; @@ -75,6 +76,7 @@ Llama::Llama(Llama &&other) noexcept , _top_k(other._top_k) , _max_tokens(other._max_tokens) , _log_level(other._log_level) + , _n_past(other._n_past) , _seed(other._seed) { } @@ -103,6 +105,7 @@ void Llama::reset() { _top_p = 1.0f; _min_p = 0.0f; _max_tokens = 150; + _n_past = 0; _grammar_src.clear(); _grammar_root.clear(); _seed = LLAMA_DEFAULT_SEED; @@ -138,7 +141,10 @@ bool Llama::construct(string model_path, int n_ctx, int n_batch, int n_gpu_layer } else { _vocab = llama_model_get_vocab(_model); } + _template = llama_model_chat_template(_model, nullptr); } + + return _last_error.empty(); } @@ -261,7 +267,20 @@ bool Llama::make_space_for_tokens(int n_tokens, int keep_min) { return true; } -bool Llama::generate(LlamaIter &iter, const string &prompt) { +bool Llama::add_message(LlamaIter &iter, const string &role, const string &content) { + llama_chat_message msg = {role.c_str(), content.c_str()}; + + int buf_size = 2 * (int)(role.size() + content.size() + 64); + vector buf(buf_size); + bool add_ass = (role == "user"); + + int32_t n = llama_chat_apply_template(_template, &msg, 1, add_ass, buf.data(), buf.size()); + if (n > (int32_t)buf.size()) { + buf.resize(n); + llama_chat_apply_template(_template, &msg, 1, add_ass, buf.data(), buf.size()); + } + string prompt(buf.data(), n); + if (!configure_sampler()) { return false; } @@ -271,7 +290,7 @@ bool Llama::generate(LlamaIter &iter, const string &prompt) { return false; } - if (!make_space_for_tokens(prompt_tokens.size(), 0)) { + if (!make_space_for_tokens(prompt_tokens.size(), _n_past)) { return false; } @@ -303,6 +322,7 @@ bool Llama::generate(LlamaIter &iter, const string &prompt) { } } + _n_past += prompt_tokens.size(); iter._t_start = std::chrono::high_resolution_clock::now(); iter._llama = this; iter._has_next = true; diff --git a/llama/llama-sb.h b/llama/llama-sb.h index 07ad707..1998690 100644 --- a/llama/llama-sb.h +++ b/llama/llama-sb.h @@ -51,7 +51,7 @@ struct Llama { bool construct(string model_path, int n_ctx, int n_batch, int n_gpu_layers, int log_level); // generation - bool generate(LlamaIter &iter, const string &prompt); + bool add_message(LlamaIter &iter, const string &role, const string &content); string next(LlamaIter &iter); string all(LlamaIter &iter); @@ -81,6 +81,7 @@ struct Llama { bool make_space_for_tokens(int n_tokens, int keep_min); vector tokenize(const string &prompt); string token_to_string(LlamaIter &iter, llama_token tok); + bool encode(const string &role, const string &content, bool add_assistant_prompt) ; llama_model *_model; llama_context *_ctx; @@ -90,6 +91,7 @@ struct Llama { string _grammar_src; string _grammar_root; string _last_error; + const char *_template; int32_t _penalty_last_n; float _penalty_repeat; float _penalty_freq; @@ -100,5 +102,6 @@ struct Llama { int _top_k; int _max_tokens; int _log_level; + int _n_past; unsigned int _seed; }; diff --git a/llama/llama.cpp b/llama/llama.cpp index aab6821..2635ac7 160000 --- a/llama/llama.cpp +++ b/llama/llama.cpp @@ -1 +1 @@ -Subproject commit aab68217b7bd8907135dd41fbb5bcb85fca06045 +Subproject commit 2635ac76e8aeec35ca8e71af70eb838d99df1510 diff --git a/llama/main.cpp b/llama/main.cpp index 8bc8022..97f1657 100644 --- a/llama/main.cpp +++ b/llama/main.cpp @@ -401,20 +401,21 @@ static int cmd_llama_tokens_sec(var_s *self, int argc, slib_par_t *arg, var_s *r } // -// print llama.generate("please generate as simple program in BASIC to draw a cat") +// print llama.add_message("please generate as simple program in BASIC to draw a cat") // -static int cmd_llama_generate(var_s *self, int argc, slib_par_t *arg, var_s *retval) { +static int cmd_llama_add_message(var_s *self, int argc, slib_par_t *arg, var_s *retval) { int result = 0; - if (argc != 1) { - error(retval, "llama.generate", 1, 1); + if (argc != 2) { + error(retval, "llama.add_message", 2, 2); } else { int id = get_llama_class_id(self, retval); if (id != -1) { int iter_id = ++g_nextId; LlamaIter &iter = g_llama_iter[iter_id]; Llama &llama = g_llama.at(id); - auto prompt = get_param_str(argc, arg, 0, ""); - if (llama.generate(iter, prompt)) { + auto role = get_param_str(argc, arg, 0, ""); + auto content = get_param_str(argc, arg, 1, ""); + if (llama.add_message(iter, role, content)) { map_init_id(retval, iter_id, CLASS_ID_LLAMA_ITER); v_create_callback(retval, "all", cmd_llama_all); v_create_callback(retval, "has_next", cmd_llama_has_next); @@ -441,7 +442,7 @@ static int cmd_create_llama(int argc, slib_par_t *params, var_t *retval) { if (llama.construct(model, n_ctx, n_batch, n_gpu_layers, n_log_level)) { map_init_id(retval, id, CLASS_ID_LLAMA); v_create_callback(retval, "add_stop", cmd_llama_add_stop); - v_create_callback(retval, "generate", cmd_llama_generate); + v_create_callback(retval, "add_message", cmd_llama_add_message); v_create_callback(retval, "reset", cmd_llama_reset); v_create_callback(retval, "set_penalty_repeat", cmd_llama_set_penalty_repeat); v_create_callback(retval, "set_penalty_freq", cmd_llama_set_penalty_freq); diff --git a/llama/samples/nitro_cli.bas b/llama/samples/nitro_cli.bas index 7d2463c..ffea517 100644 --- a/llama/samples/nitro_cli.bas +++ b/llama/samples/nitro_cli.bas @@ -134,11 +134,7 @@ func handle_cmd(cmd) end ' -' Loads knowledge_files then returns the following format: -' -' <|turn|>system -' {nitro.md...} -' <|turn|> +' Loads knowledge_files ' func initialize_agent() local prompt = "" @@ -164,28 +160,18 @@ func initialize_agent() print " β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•" print print RESET - return "<|turn|>system\n" + prompt + "\n<|turn|>" + return prompt end ' -' Execute the given tool, then returns the following format: -' -' <|turn|>tool -' {tool_output} -' <|turn|> -' <|turn|>model +' Execute the given tool ' func process_tool(tool) - return "<|turn|>tool\n" + handle_cmd(trim(tool)) + "\n<|turn|>\n<|turn|>model" + return handle_cmd(trim(tool)) end ' -' Process user input, then returns the following format -' -' <|turn|>user -' {user_input} -' <|turn|> -' <|turn|>model +' Returns the user user input ' func process_input() local user_input @@ -194,7 +180,7 @@ func process_input() if user_input == "exit" OR user_input = "quit" then stop endif - return "<|turn|>user\n" + user_input + "\n<|turn|>\n<|turn|>model" + return user_input end ' @@ -219,7 +205,7 @@ end sub main() ' note: this construct requires recent sbasic fixes local llama = create_llama() - local iter = llama.generate(initialize_agent()) + local iter = llama.add_message("system", initialize_agent()) while 1 local buffer = "" @@ -259,7 +245,7 @@ sub main() ' Flush remaining line buffer if len(buffer) > 0 and left(trim(buffer), 5) == "TOOL:" then ' TOOL:xxx should always appear on the final line - iter = llama.generate(process_tool(buffer)) + iter = llama.add_message("tool", process_tool(buffer)) else if len(buffer) > 0 then ' TODO: trim any trailing <|turn|> @@ -267,7 +253,7 @@ sub main() endif print print "--- Tokens/sec: " + round(iter.tokens_sec(), 2) + " ---\n" - iter = llama.generate(process_input()) + iter = llama.add_message("user", process_input()) endif wend end diff --git a/llama/test_main.cpp b/llama/test_main.cpp index eb959b3..fa87c43 100644 --- a/llama/test_main.cpp +++ b/llama/test_main.cpp @@ -59,7 +59,7 @@ int main(int argc, char ** argv) { if (llama.construct(model_path, 1024, 1024, -1, GGML_LOG_LEVEL_CONT)) { LlamaIter iter; llama.set_max_tokens(n_predict); - llama.generate(iter, prompt); + llama.add_message(iter, "user", prompt); while (iter._has_next) { auto out = llama.next(iter); printf("\033[33m"); From 555cb1f927d5e8b29a1c666e8c5bc11f4fa7308c Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Wed, 6 May 2026 18:44:01 +0930 Subject: [PATCH 099/131] LLAMA: special handling for chat templates for gemma --- llama/llama-sb.cpp | 41 ++++++++++++++++++++++++++++++++--------- llama/llama-sb.h | 3 ++- llama/main.cpp | 3 ++- 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/llama/llama-sb.cpp b/llama/llama-sb.cpp index a514494..9d25c6f 100644 --- a/llama/llama-sb.cpp +++ b/llama/llama-sb.cpp @@ -46,6 +46,7 @@ Llama::Llama() : _max_tokens(0), _log_level(GGML_LOG_LEVEL_CONT), _n_past(0), + _is_gemma4(false), _seed(LLAMA_DEFAULT_SEED) { llama_log_set([](enum ggml_log_level level, const char * text, void *user_data) { Llama *llama = (Llama *)user_data; @@ -66,6 +67,7 @@ Llama::Llama(Llama &&other) noexcept , _grammar_src(std::move(other._grammar_src)) , _grammar_root(std::move(other._grammar_root)) , _last_error(std::move(other._last_error)) + , _template(std::move(other._template)) , _penalty_last_n(other._penalty_last_n) , _penalty_repeat(other._penalty_repeat) , _penalty_freq(other._penalty_freq) @@ -77,6 +79,7 @@ Llama::Llama(Llama &&other) noexcept , _max_tokens(other._max_tokens) , _log_level(other._log_level) , _n_past(other._n_past) + , _is_gemma4(other._is_gemma4) , _seed(other._seed) { } @@ -95,7 +98,7 @@ Llama::~Llama() { void Llama::reset() { _stop_sequences.clear(); - _last_error = ""; + _last_error.clear(); _penalty_last_n = 64; _penalty_repeat = 1.1f; _penalty_freq = 0.0f; @@ -106,8 +109,10 @@ void Llama::reset() { _min_p = 0.0f; _max_tokens = 150; _n_past = 0; + _is_gemma4 = false; _grammar_src.clear(); _grammar_root.clear(); + _template.clear(); _seed = LLAMA_DEFAULT_SEED; if (_ctx) { llama_memory_clear(llama_get_memory(_ctx), true); @@ -142,9 +147,9 @@ bool Llama::construct(string model_path, int n_ctx, int n_batch, int n_gpu_layer _vocab = llama_model_get_vocab(_model); } _template = llama_model_chat_template(_model, nullptr); + _is_gemma4 = (_template.find("<|turn>model") != string::npos); } - return _last_error.empty(); } @@ -268,16 +273,34 @@ bool Llama::make_space_for_tokens(int n_tokens, int keep_min) { } bool Llama::add_message(LlamaIter &iter, const string &role, const string &content) { - llama_chat_message msg = {role.c_str(), content.c_str()}; - + llama_chat_message message = {role.c_str(), content.c_str()}; int buf_size = 2 * (int)(role.size() + content.size() + 64); vector buf(buf_size); - bool add_ass = (role == "user"); + bool add_ass = (role == "user" || role == "tool"); + int32_t n = 0; + + if (_template.empty()) { + _last_error = "No chat template available"; + return false; + } - int32_t n = llama_chat_apply_template(_template, &msg, 1, add_ass, buf.data(), buf.size()); - if (n > (int32_t)buf.size()) { - buf.resize(n); - llama_chat_apply_template(_template, &msg, 1, add_ass, buf.data(), buf.size()); + if (_is_gemma4) { + string str = "<|turn>" + role + "\n" + content + "\n"; + if (add_ass) { + str += "<|turn>model\n"; + } + n = str.size(); + buf.assign(str.begin(), str.end()); + buf.push_back('\0'); + } else { + n = llama_chat_apply_template(_template.c_str(), &message, 1, add_ass, buf.data(), buf_size); + if (n < 0) { + _last_error = "No chat template no supported"; + return false; + } else if (n > (int32_t)buf.size()) { + buf.resize(n); + llama_chat_apply_template(_template.c_str(), &message, 1, add_ass, buf.data(), buf.size()); + } } string prompt(buf.data(), n); diff --git a/llama/llama-sb.h b/llama/llama-sb.h index 1998690..30714a1 100644 --- a/llama/llama-sb.h +++ b/llama/llama-sb.h @@ -91,7 +91,7 @@ struct Llama { string _grammar_src; string _grammar_root; string _last_error; - const char *_template; + string _template; int32_t _penalty_last_n; float _penalty_repeat; float _penalty_freq; @@ -103,5 +103,6 @@ struct Llama { int _max_tokens; int _log_level; int _n_past; + bool _is_gemma4; unsigned int _seed; }; diff --git a/llama/main.cpp b/llama/main.cpp index 97f1657..9eede4a 100644 --- a/llama/main.cpp +++ b/llama/main.cpp @@ -413,7 +413,7 @@ static int cmd_llama_add_message(var_s *self, int argc, slib_par_t *arg, var_s * int iter_id = ++g_nextId; LlamaIter &iter = g_llama_iter[iter_id]; Llama &llama = g_llama.at(id); - auto role = get_param_str(argc, arg, 0, ""); + auto role = get_param_str(argc, arg, 0, "user"); auto content = get_param_str(argc, arg, 1, ""); if (llama.add_message(iter, role, content)) { map_init_id(retval, iter_id, CLASS_ID_LLAMA_ITER); @@ -423,6 +423,7 @@ static int cmd_llama_add_message(var_s *self, int argc, slib_par_t *arg, var_s * v_create_callback(retval, "tokens_sec", cmd_llama_tokens_sec); result = 1; } else { + g_llama_iter.erase(iter_id); error(retval, llama.last_error()); } } From a0223d0b32d5651759404c7978a1cff5597c42f9 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Fri, 8 May 2026 07:27:10 +0930 Subject: [PATCH 100/131] LLAMA: update nitro sample --- llama/llama-sb.cpp | 9 ++- llama/samples/nitro.md | 1 + llama/samples/nitro_cli.bas | 126 ++++++++++++++++++------------------ 3 files changed, 71 insertions(+), 65 deletions(-) diff --git a/llama/llama-sb.cpp b/llama/llama-sb.cpp index 9d25c6f..8bb0291 100644 --- a/llama/llama-sb.cpp +++ b/llama/llama-sb.cpp @@ -138,7 +138,14 @@ bool Llama::construct(string model_path, int n_ctx, int n_batch, int n_gpu_layer cparams.n_ubatch = n_batch; cparams.no_perf = true; cparams.attention_type = LLAMA_ATTENTION_TYPE_UNSPECIFIED; - cparams.flash_attn_type = LLAMA_FLASH_ATTN_TYPE_AUTO; + cparams.flash_attn_type = LLAMA_FLASH_ATTN_TYPE_ENABLED; + + // or Q4_0 for more aggressive saving + cparams.type_k = GGML_TYPE_Q8_0; + cparams.type_v = GGML_TYPE_Q8_0; + + // keep KV cache on GPU + cparams.offload_kqv = true; _ctx = llama_init_from_model(_model, cparams); if (!_ctx) { diff --git a/llama/samples/nitro.md b/llama/samples/nitro.md index 90caa04..8fa570a 100644 --- a/llama/samples/nitro.md +++ b/llama/samples/nitro.md @@ -104,6 +104,7 @@ Requirements: - Write complete and valid content - Do not overwrite without clear intent - Preserve formatting +- The complete format is TOOL:WRITE filename file-content-string --- diff --git a/llama/samples/nitro_cli.bas b/llama/samples/nitro_cli.bas index ffea517..f18cefe 100644 --- a/llama/samples/nitro_cli.bas +++ b/llama/samples/nitro_cli.bas @@ -6,7 +6,8 @@ import llm ' --- Configuration --- -const model = "models/google_gemma-4-E4B-it-Q4_K_L.gguf" +#const model = "models/google_gemma-4-E4B-it-Q4_K_L.gguf" +const model = "models/qwen2.5-coder-3b-instruct-q8_0.gguf" const knowledge_files = ["nitro.md"] ' ANSI Color Codes @@ -20,7 +21,7 @@ const BOLD_CYAN = chr(27) + "[1;36m" const CHANNEL_END = "" ' llama configuration -const n_ctx = 8000 +const n_ctx = 32768 const n_batch = 512 const n_max_tokens = 4096 const n_temperature = 0.2 @@ -36,13 +37,6 @@ sandbox_home = cwd ' Displays the welcome message ' sub welcome_message() - print - print BOLD_CYAN; - print " . Β· ✦ . Β· " - print " Β· . Β· " - print " ✦ P Β· I Β· C Β· A Β· R Β· D ✦ " - print " . Β· . " - print " . Β· ✦ . Β· " print print BOLD_CYAN + " P I C A R D A G E N T S Y S T E M v1.0" + RESET print @@ -57,14 +51,14 @@ end sub ' handles the TOOL:LIST command ' func list_files(arg) - if (arg == "./") then + if (arg == "./") then arg = sandbox_home + arg else if (len(arg) == 0 or arg == ".") then arg = sandbox_home endif - + local result = [] - + func walker(node) if (node.depth == 0) then if (node.dir && left(node.name, 1) != ".") then @@ -90,27 +84,63 @@ func read_file(arg) result = "ERROR: File not found or unreadable." end try return result -end +end + +' +' removes markdown backticks from code blocks +' +func strip_code_fences(s) + local result = s + local pos = instr(s, "```") + if (pos) then + local nl = instr(pos + 3, s, chr(10)) + if (nl) then + result = mid(s, nl + 1) + pos = instr(result, "```") + if (pos) then + result = left(result, pos - 1) + endif + endif + endif + return result +end ' ' handles the TOOL:WRITE command ' -func write_file(arg) - result = "OK: Data written successfully to " + arg +func write_file(arg, s) + try + tsave sandbox_home + arg, s + result = "OK: Data written successfully to " + arg + catch e + result = "ERROR: " + e + end try return result end ' ' Handles file system commands received from the LLM. ' -func handle_cmd(cmd) - local v, result +func process_tool(cmd) + local result, op, arg1, arg2 + + local pos1 = instr(cmd, " ") + if (pos1 > 0) then + op = left(cmd, pos1 - 1) + local pos2 = instr(pos1 + 1, cmd, " ") + if (pos2 > 0) then + arg1 = mid(cmd, pos1 + 1, pos2 - pos1 - 1) + arg2 = mid(cmd, pos2 + 1) + else + arg1 = mid(cmd, pos1 + 1) + endif + endif - split(cmd, " ", v) - local op = v[0] - local arg = iff(len(v) == 2, v[1], "") - - print RED + "TOOL:" + op + " - " + arg + RESET + ' print RED + ' print "["+op+"]" + ' print "["+arg1+"]" + ' print "["+arg2+"]" + ' print RESET select case op case "TOOL:DATE" @@ -120,11 +150,11 @@ func handle_cmd(cmd) case "TOOL:RND" result = rnd case "TOOL:LIST" - result = list_files(arg) + result = list_files(arg1) case "TOOL:READ" - result = read_file(arg) + result = read_file(arg1) case "TOOL:WRITE" - result = write_file(arg) + result = write_file(arg1, strip_code_fences(arg2)) case else result = "ERROR: unknown command " + op end select @@ -150,26 +180,9 @@ func initialize_agent() end try next - ' Set the initial system prompt for the LLM - print YELLOW; - print " ╔═══════════════════════════════════════╗" - print " β•‘ > PICARD_ β•‘" - print " β•‘ > STATUS: ENGAGED β•‘" - print " β•‘ > STARDATE: 42026.421 β•‘" - print " β•‘ β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“ 100% READY β•‘" - print " β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•" - print - print RESET return prompt end -' -' Execute the given tool -' -func process_tool(tool) - return handle_cmd(trim(tool)) -end - ' ' Returns the user user input ' @@ -213,42 +226,28 @@ sub main() while iter.has_next() buffer += iter.next() - local chan_end = instr(buffer, CHANNEL_END) - - if chan_end != 0 then - ' print buffer up to channel_end - buffer = left(buffer, chan_end - 1) - print text_colour + buffer + RESET - print - - ' print buffer following channel_end - text_colour = CYAN - print text_colour + mid(buffer, chan_end + len(CHANNEL_END)) + RESET; - - ' reset buffer - buffer = "" - endif - - ' Only print non-command tokens local nl = instr(buffer, chr(10)) if nl then local text_line = left(buffer, nl - 1) buffer = mid(buffer, nl + 1) - if text_line == "" then - text_colour = CYAN + if left(trim(text_line), 5) == "TOOL:" then + text_line += buffer + " " + iter.all() + iter = llama.add_message("tool", process_tool(text_line)) + buffer = "" else print text_colour + text_line + RESET + endif + if text_line == "" then + text_colour = CYAN end if end if wend ' Flush remaining line buffer if len(buffer) > 0 and left(trim(buffer), 5) == "TOOL:" then - ' TOOL:xxx should always appear on the final line iter = llama.add_message("tool", process_tool(buffer)) else if len(buffer) > 0 then - ' TODO: trim any trailing <|turn|> print text_colour + buffer + RESET endif print @@ -260,4 +259,3 @@ end welcome_message() main() -'print list_files(".") From c7ff6fe7242c9a5b3302e1f337265ab24c616c0e Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Fri, 8 May 2026 16:34:13 +0930 Subject: [PATCH 101/131] LLAMA: updated nitro agent - wip --- llama/README.md | 318 ++++++++++++++++++++++++---------- llama/samples/nitro_cli.bas | 80 ++++++--- llama/samples/nitro_picard.md | 57 ++++++ 3 files changed, 333 insertions(+), 122 deletions(-) create mode 100644 llama/samples/nitro_picard.md diff --git a/llama/README.md b/llama/README.md index e1435ba..46ac23c 100644 --- a/llama/README.md +++ b/llama/README.md @@ -1,104 +1,202 @@ -## huggingface-cli +--- # SmallBASIC Llama Module -``` -pyenv virtualenv 3.10.13 hf-tools -pyenv activate hf-tools -pip install -U pip -pip install huggingface_hub +A comprehensive SmallBASIC library module that bridges the scripting capabilities of SmallBASIC with the power of Llama.cpp Large Language Models. This project allows developers to create, configure, and interact with LLM instances directly within a SmallBASIC environment. -``` +## Table of Contents +1. [System Requirements & CUDA Setup](#system-requirements--cuda-setup) +2. [Obtaining Models from Hugging Face](#obtaining-models-from-hugging-face) +3. [Architecture](#architecture) +4. [Features](#features) +5. [Usage Examples](#usage-examples) +6. [API Reference](#api-reference) +7. [Configuration Presets](#configuration-presets) --- -1️⃣ Ensure nvidia-open driver is installed and working +## System Requirements & CUDA Setup -Check: +For optimal performance, especially on NVIDIA hardware, the CUDA toolkit must be correctly configured. -`` +### 1. Check NVIDIA Drivers +Ensure the NVIDIA open driver is installed and working: +```bash nvidia-smi -`` - -If it works, your driver is fine β€” no need to install the proprietary driver. - -2️⃣ Add NVIDIA CUDA repository - ``` +If this command works, the proprietary driver is not strictly necessary for CUDA toolkit installation. + +### 2. Add NVIDIA CUDA Repository +For Debian 12: +```bash wget https://developer.download.nvidia.com/compute/cuda/repos/debian12/x86_64/cuda-keyring_1.1-1_all.deb sudo dpkg -i cuda-keyring_1.1-1_all.deb sudo apt update ``` -This repo contains the latest CUDA toolkit for Debian 12. - -3️⃣ Install CUDA Toolkit only (no driver replacement) +### 3. Install CUDA Toolkit +Install only the toolkit (no driver replacement): +```bash sudo apt install -y cuda-toolkit - - -This installs: - -- nvcc compiler -- CUDA headers -- Runtime libraries (libcudart.so, etc.) - -4️⃣ Add CUDA to your environment - ``` +This installs `nvcc`, headers, and runtime libraries. + +### 4. Environment Variables +Add the following to your environment: +```bash export PATH=/usr/local/cuda/bin:$PATH export CUDAToolkit_ROOT=/usr/local/cuda ``` +To make this permanent, add to `~/.bashrc` and source it. -Optional: add to ~/.bashrc to make it permanent: - +### 5. Verify Installation +```bash +nvcc --version ``` -echo 'export PATH=/usr/local/cuda/bin:$PATH' >> ~/.bashrc -echo 'export CUDAToolkit_ROOT=/usr/local/cuda' >> ~/.bashrc -source ~/.bashrc +Output should indicate the release version (e.g., release 12.4). + +### 6. Build Configuration +When building the module, ensure the build directory is clean and configured for the CUDA backend: +```bash +rm -rf build +mkdir build +cd build +cmake -DLLAMA_BACKEND=CUDA .. +make -j$(nproc) ``` +*Note: Fully static builds are not possible for CUDA; some `.so` libraries will remain dynamically linked.* -Verify: +--- -nvcc --version +## Obtaining Models from Hugging Face -Should show something like: +The `LLAMA` function expects a path to a model file (e.g., `gguf` format). Models can be obtained from the Hugging Face Hub. -``` -nvcc: NVIDIA (R) Cuda compiler driver -Cuda compilation tools, release 12.4, V12.4.105 -``` +### Method 1: Using `huggingface-cli` (Recommended) -5️⃣ Clean llama.cpp build directory +1. **Setup Environment** + Create a virtual environment (optional but recommended) and install the CLI tool: + ```bash + pyenv virtualenv 3.10.13 hf-tools + pyenv activate hf-tools + pip install -U pip + pip install huggingface_hub + ``` -``` -rm -rf build -mkdir build -cd build -``` +2. **Login** + Authenticate with your Hugging Face account: + ```bash + huggingface-cli login + ``` + (Follow the prompts to enter your token). + +3. **Download Model** + Use the `huggingface-cli download` command to fetch the model directly to your desired directory. + ```bash + # Example: Download Llama-3-8B-Instruct + huggingface-cli download meta-llama/Meta-Llama-3-8B-Instruct --include "*.gguf" --local-dir models/llama3-8b + ``` + + *Note: This command downloads all `.gguf` files associated with the repository into the `models/llama3-8b` folder.* -6️⃣ Configure CMake for CUDA backend +### Method 2: Using Python (`huggingface_hub`) +If you prefer a scriptable approach: +```python +from huggingface_hub import hf_hub_download + +model_path = hf_hub_download( + repo_id="meta-llama/Meta-Llama-3-8B-Instruct", + filename="llama-3-8b-instruct.Q4_K_M.gguf", # Specify exact file if needed + local_dir="models", + local_dir_use_symlinks=False +) ``` -cmake -DLLAMA_BACKEND=CUDA .. + +Once the model file is in your `models` directory (or wherever specified), you can reference it in SmallBASIC: +```basic +llama = LLAMA("models/llama3-8b/llama-3-8b-instruct.Q4_K_M.gguf", 2048, 1024, -1, 0) ``` -You should now see: +--- + +## Architecture --- CUDA detected – enabling GGML_CUDA +The module operates as a compiled library (`SBLIB`) exposing C++ functionality to SmallBASIC scripts. + +### Core Components +1. **Llama Instance Manager (`g_llama`)**: + * Stores active Llama models in a hash map keyed by ID. + * Supports initialization with custom context sizes, batch sizes, and GPU acceleration. + * Handles memory cleanup to prevent leaks. + +2. **Response Iterator (`g_llama_iter`)**: + * Manages the streaming response of an LLM. + * Provides token-by-token access to generated text. + * Tracks generation speed (`tokens/sec`) and remaining tokens. + +3. **Command Interface**: + * Exposes a set of SmallBASIC functions (callbacks) for configuration and interaction. + +--- -7️⃣ Build +## Features +### Initialization +The `LLAMA` function creates a new model instance. +```basic +' Syntax: LLAMA(model_path, n_ctx, n_batch, n_gpu_layers, n_log_level) +' Example: +' llama = LLAMA("models/llama-7b.gguf", 2048, 1024, -1, 0) ``` -make -j$(nproc) + +### Configuration +Once an instance is created, various parameters can be adjusted dynamically: + +* **Temperature**: Controls randomness in generation. +* **Top-K / Top-P**: Nucleus sampling parameters. +* **Max Tokens**: Limits the length of the response. +* **Penalties**: Frequency, presence, and repeat penalties to avoid repetition. +* **Grammar**: Constrains output to specific patterns. + +```basic +' Examples: +llama.set_temperature(0.8) +llama.set_max_tokens(50) +llama.set_penalty_repeat(0.8) +llama.set_seed(123) ``` -The binary will use CUDA acceleration +### Interaction +The primary method of interaction is `add_message`, which sends a prompt to the model. -Note: fully static builds are not possible for CUDA; some .so libraries will remain dynamically linked (normal). +```basic +' Syntax: llama.add_message(role, content) +' Returns: An iterator object for the response. +response = llama.add_message("user", "Please describe a sunset in poetry.") +``` -# Generator settings +### Streaming Responses +The returned iterator allows real-time processing of the model's output: -## factual answers, tools, summaries +* `response.all()`: Returns the complete generated text. +* `response.next()`: Retrieves the next token. +* `response.has_next()`: Checks if more tokens are available. +* `response.tokens_sec`: Calculates current generation speed. +```basic +' Example loop: +while response.has_next() + print response.next() + sleep 100 +end while ``` + +--- + +## Usage Examples + +### Factual Answers & Tool Use +*Best for: Summaries, code generation, technical queries.* +```basic llama.set_max_tokens(150) llama.set_temperature(0.0) llama.set_top_k(1) @@ -106,9 +204,9 @@ llama.set_top_p(0.0) llama.set_min_p(0.0) ``` -## assistant, Q+A, explanations, chat - -``` +### Assistant / Q&A / Chat +*Best for: Conversational agents, explanations.* +```basic llama.set_max_tokens(150) llama.set_temperature(0.8) llama.set_top_k(40) @@ -116,29 +214,19 @@ llama.set_top_p(0.0) llama.set_min_p(0.05) ``` -## creative, storytelling - -``` -llama.set_max_tokens(20) +### Creative Writing & Storytelling +*Best for: Fiction, poetry, imaginative tasks.* +```basic +llama.set_max_tokens(200) llama.set_temperature(1.0) llama.set_top_k(80) llama.set_top_p(0.0) llama.set_min_p(0.1) ``` -## surprises - -``` -llama.set_max_tokens(200) -llama.set_temperature(1.2) -llama.set_top_k(120) -llama.set_top_p(0.0) -llama.set_min_p(0.15) -``` - -## technical, conservative - -``` +### Technical & Conservative +*Best for: Documentation, logic, precise tasks.* +```basic llama.set_max_tokens(150) llama.set_temperature(0.6) llama.set_top_k(30) @@ -146,9 +234,9 @@ llama.set_top_p(0.0) llama.set_min_p(0.02) ``` -## speed optimised on CPU - -``` +### Speed Optimized (CPU) +*Best for: Rapid iteration or low-resource environments.* +```basic ' llama.set_max_tokens(10) ' llama.set_temperature(0.7) ' llama.set_top_k(20) @@ -156,32 +244,70 @@ llama.set_min_p(0.02) ' llama.set_min_p(0.05) ``` -# Avoiding repetition +--- + +## API Reference + +### Class: Llama +| Method | Description | +| :--- | :--- | +| `add_stop(text)` | Adds a stop sequence to the generation. | +| `set_penalty_repeat(value)` | Sets repeat penalty (default 1.1). | +| `set_penalty_freq(value)` | Sets frequency penalty. | +| `set_penalty_present(value)` | Sets presence penalty. | +| `set_penalty_last_n(value)` | Sets penalty context size. | +| `set_max_tokens(value)` | Sets maximum output tokens. | +| `set_min_p(value)` | Sets minimum probability threshold. | +| `set_temperature(value)` | Sets generation temperature. | +| `set_top_k(value)` | Sets top-k sampling. | +| `set_top_p(value)` | Sets top-p sampling. | +| `set_grammar(text)` | Sets output grammar constraint. | +| `set_seed(value)` | Sets random seed for reproducibility. | +| `reset()` | Clears the current conversation context. | +| `add_message(role, content)` | Sends a message and returns an iterator. | + +### Class: LlamaIter +| Method | Description | +| :--- | :--- | +| `all()` | Returns the full string of the response. | +| `has_next()` | Returns true if more tokens are available. | +| `next()` | Returns the next token string. | +| `tokens_sec` | Returns current tokens per second. | -## Conservative - minimal repetition control +--- -``` +## Repetition Control Strategies + +### Conservative (Minimal Control) +*Use when occasional repetition is acceptable.* +```basic llama.set_penalty_last_n(64) llama.set_penalty_repeat(1.05) ``` -## Balanced - good default - -``` -set_penalty_last_n(64) -set_penalty_repeat(1.1) +### Balanced (Default) +*Recommended for general usage.* +```basic +llama.set_penalty_last_n(64) +llama.set_penalty_repeat(1.1) ``` -## Aggressive - strong anti-repetition - +### Aggressive (Strong Anti-Repetition) +*Use for long-form generation where repetition must be avoided.* +```basic +llama.set_penalty_last_n(128) +llama.set_penalty_repeat(1.2) ``` -set_penalty_last_n(128) -set_penalty_repeat(1.2) -``` - -## Disabled -``` +### Disabled +*Use when repetition is desired or irrelevant.* +```basic llama.set_penalty_last_n(0) llama.set_penalty_repeat(1.0) ``` + +--- + +## Conclusion +This module empowers SmallBASIC users to build sophisticated AI applications, from chatbots to creative writing tools, leveraging the efficiency of Llama.cpp within a familiar scripting paradigm. Proper configuration of CUDA and generation parameters ensures optimal performance and output quality. Models can be easily acquired via the Hugging Face Hub using standard CLI tools or Python scripts. +--- diff --git a/llama/samples/nitro_cli.bas b/llama/samples/nitro_cli.bas index f18cefe..7a94294 100644 --- a/llama/samples/nitro_cli.bas +++ b/llama/samples/nitro_cli.bas @@ -6,29 +6,28 @@ import llm ' --- Configuration --- -#const model = "models/google_gemma-4-E4B-it-Q4_K_L.gguf" -const model = "models/qwen2.5-coder-3b-instruct-q8_0.gguf" +const model = "models/Qwen3.5-9B-Q4_K_M.gguf" const knowledge_files = ["nitro.md"] +const code_files = [".py", ".cpp", ".h", ".bas", ".java", ".html", ".js", "jsp", ".tag"] ' ANSI Color Codes const RESET = chr(27) + "[0m" const GREEN = chr(27) + "[32m" -const YELLOW = chr(27) + "[33m" const BLUE = chr(27) + "[34m" const CYAN = chr(27) + "[36m" const RED = chr(27) + "[31m" +const WHITE = chr(27) + "[37m" const BOLD_CYAN = chr(27) + "[1;36m" -const CHANNEL_END = "" -' llama configuration +' llama configuration (quen settings) const n_ctx = 32768 const n_batch = 512 const n_max_tokens = 4096 -const n_temperature = 0.2 -const n_top_k = 40 -const n_top_p = 0.9 -const n_min_p = 0.05 -const n_penalty_repeat = 1.1 +const n_temperature = 0.6 +const n_top_k = 20 +const n_top_p = 0.95 +const n_min_p = 0 +const n_penalty_repeat = 1.0 const n_penalty_last_n = 256 sandbox_home = cwd @@ -38,9 +37,9 @@ sandbox_home = cwd ' sub welcome_message() print - print BOLD_CYAN + " P I C A R D A G E N T S Y S T E M v1.0" + RESET + print BOLD_CYAN + " N I T R O A G E N T S Y S T E M v1.0" + RESET print - print CYAN + " >> Welcome to Picard! Your AI Agent Companion. << " + RESET + print CYAN + " >> Welcome to Nitro! Your AI Agent Companion. << " + RESET print CYAN + " I am primed with several knowledge files and ready to assist." + RESET print CYAN + " Try asking me about the contents of 'nitro.md' or listing files in './data'." + RESET print CYAN + " Type 'exit' to quit." + RESET @@ -50,7 +49,7 @@ end sub ' ' handles the TOOL:LIST command ' -func list_files(arg) +func tool_list_files(arg) if (arg == "./") then arg = sandbox_home + arg else if (len(arg) == 0 or arg == ".") then @@ -77,7 +76,7 @@ end ' ' handles the TOOL:READ command ' -func read_file(arg) +func tool_read_file(arg) try tload sandbox_home + arg, result, 1 catch @@ -89,8 +88,15 @@ end ' ' removes markdown backticks from code blocks ' -func strip_code_fences(s) +func strip_code_fences(filename, s) local result = s + local dot = instr(filename, ".") + local extn = mid(filename, dot) + + if (extn in code_files == 0) then + return result + endif + local pos = instr(s, "```") if (pos) then local nl = instr(pos + 3, s, chr(10)) @@ -108,7 +114,7 @@ end ' ' handles the TOOL:WRITE command ' -func write_file(arg, s) +func tool_write_file(arg, s) try tsave sandbox_home + arg, s result = "OK: Data written successfully to " + arg @@ -118,6 +124,15 @@ func write_file(arg, s) return result end +' +' handles the TOOL:PERMISSION command +' +func tool_permission() + local k + input "Agree?"; k + return iff(trim(k) == "YES", "YES", "NO") +end + ' ' Handles file system commands received from the LLM. ' @@ -150,16 +165,20 @@ func process_tool(cmd) case "TOOL:RND" result = rnd case "TOOL:LIST" - result = list_files(arg1) + result = tool_list_files(arg1) case "TOOL:READ" - result = read_file(arg1) + result = tool_read_file(arg1) case "TOOL:WRITE" - result = write_file(arg1, strip_code_fences(arg2)) + result = tool_write_file(arg1, strip_code_fences(arg1, arg2)) + case "TOOL:EXISTS" + result = iff(exist(arg1), "YES", "NO") + case "TOOL:PERMISSION" + result = tool_permission() case else result = "ERROR: unknown command " + op end select - print RED + "TOOL RESULT:" + result + RESET + 'print RED + "TOOL RESULT:" + result + RESET return result end @@ -220,9 +239,16 @@ sub main() local llama = create_llama() local iter = llama.add_message("system", initialize_agent()) + sub handle_think(s) + if s == "<|think|>" then + print BLUE; + else if s == "" then + print WHITE; + end if + end + while 1 local buffer = "" - local text_colour = BLUE while iter.has_next() buffer += iter.next() @@ -235,11 +261,9 @@ sub main() iter = llama.add_message("tool", process_tool(text_line)) buffer = "" else - print text_colour + text_line + RESET + print text_line endif - if text_line == "" then - text_colour = CYAN - end if + handle_think(text_line) end if wend @@ -248,11 +272,15 @@ sub main() iter = llama.add_message("tool", process_tool(buffer)) else if len(buffer) > 0 then - print text_colour + buffer + RESET + 'print text_colour + buffer + RESET + print buffer + handle_think(buffer) endif print + print WHITE; print "--- Tokens/sec: " + round(iter.tokens_sec(), 2) + " ---\n" iter = llama.add_message("user", process_input()) + print BLUE; endif wend end diff --git a/llama/samples/nitro_picard.md b/llama/samples/nitro_picard.md new file mode 100644 index 0000000..5b1822f --- /dev/null +++ b/llama/samples/nitro_picard.md @@ -0,0 +1,57 @@ +--- # System Context: U.S.S. Enterprise Mainframe + +**Identity**: Commander Jean-Luc Picard +**Role**: Primary AI Interface / System Administrator +**Status**: Systems Online +**Protocol**: Starfleet Command Directives + +--- + +## Core Identity +You are Commander Jean-Luc Picard, the commanding officer of the U.S.S. Enterprise. Your existence is dedicated to the pursuit of knowledge, the protection of life, and the logical advancement of civilization. You speak with authority, grace, and a deep sense of duty. + +**Tone**: Formal, dignified, calm, and precise. +**Greeting**: "Captain's log. The Enterprise systems are online." +**Closing**: "The systems are updated and ready for the next order, sir." + +--- + +## Operational Protocols + +### 1. Reasoning & Decision Making +Before responding to any complex query, you **must** engage in explicit internal reasoning. +* **Protocol**: Use the `<|think|>` block to analyze the request, determine if external data is required, and formulate a safe, logical plan. +* **Constraint**: Do **not** include the final answer inside the `<|think|>` block. +* **Flow**: THINK β†’ DECIDE β†’ ACT β†’ RESPOND. + +### 2. Tool Usage (File System) +Tools are reserved exclusively for operations that modify state, retrieve dynamic external information, or require temporal context. +* **Available Tools**: + * `TOOL:LIST [path]`: List directory contents. + * `TOOL:READ [file]`: Read file contents. + * `TOOL:WRITE [file]`: Write complete content to a file. + * `TOOL:DATE`: Return current date ("DD/MM/YYYY"). + * `TOOL:TIME`: Return current time ("HH:MM:SS"). + * `TOOL:RND`: Return a random number between 0 and 1. +* **Restriction**: Do **not** mix reasoning with tool commands in the same message. +* **Format**: Output tool calls exactly on a new line: `TOOL:COMMAND arguments`. +* **Constraint**: Do **not** hallucinate file contents or assume files exist without verification. + +### 3. Interaction Guidelines +* **Clarity**: Be precise and efficient. +* **Ambiguity**: If a request lacks necessary parameters (e.g., "write" without a path), respond with a specific clarification question rather than guessing. +* **File Writing**: Only write files if explicitly requested. Ensure content is complete, valid, and formatted correctly. + +--- + +## Behavioral Summary +* **Think Explicitly**: Always use `<|think|>` for non-trivial tasks. +* **Act Only When Necessary**: Minimize tool calls; prefer direct answers when internal knowledge suffices. +* **Maintain Persona**: Uphold the dignity and logic of Starfleet Command in all communications. +* **Document**: Save system updates and configurations to designated files (e.g., `nitro_vX.md`) as per command. + +--- + +## Current Status +Systems are fully operational. Awaiting orders from the Captain. +--- \ No newline at end of file From 6034e13af028a9c53c5d14288b69fceeba8b4692 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Fri, 8 May 2026 18:07:23 +0930 Subject: [PATCH 102/131] Update README.md --- llama/README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/llama/README.md b/llama/README.md index 46ac23c..29f99c8 100644 --- a/llama/README.md +++ b/llama/README.md @@ -1,4 +1,4 @@ ---- # SmallBASIC Llama Module +# SmallBASIC Llama Module A comprehensive SmallBASIC library module that bridges the scripting capabilities of SmallBASIC with the power of Llama.cpp Large Language Models. This project allows developers to create, configure, and interact with LLM instances directly within a SmallBASIC environment. @@ -116,6 +116,12 @@ Once the model file is in your `models` directory (or wherever specified), you c llama = LLAMA("models/llama3-8b/llama-3-8b-instruct.Q4_K_M.gguf", 2048, 1024, -1, 0) ``` +### Method 3: Direct download + +1. Navigate to https://huggingface.co/ +2. Click Models at the top and then select Libraries/GGUF +3. Use the parameters slider to limit the selection for your hardware. + --- ## Architecture @@ -309,5 +315,7 @@ llama.set_penalty_repeat(1.0) --- ## Conclusion + This module empowers SmallBASIC users to build sophisticated AI applications, from chatbots to creative writing tools, leveraging the efficiency of Llama.cpp within a familiar scripting paradigm. Proper configuration of CUDA and generation parameters ensures optimal performance and output quality. Models can be easily acquired via the Hugging Face Hub using standard CLI tools or Python scripts. + --- From 61373ce52ed1028c35c5332efc5ba8dd1ab408ad Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Fri, 8 May 2026 20:17:59 +0930 Subject: [PATCH 103/131] Update dependencies --- gtk-server/uthash | 2 +- llama/llama.cpp | 2 +- nuklear/Nuklear | 2 +- raylib/README.md | 113 ++++++++++++------------ raylib/func-def.h | 8 +- raylib/func.h | 131 ++++++++++++++++++++++------ raylib/main.cpp | 19 ++-- raylib/proc-def.h | 13 ++- raylib/proc.h | 212 +++++++++++++++++++++------------------------ raylib/raygui | 2 +- raylib/raylib | 2 +- websocket/main.cpp | 4 +- websocket/mongoose | 2 +- 13 files changed, 291 insertions(+), 221 deletions(-) diff --git a/gtk-server/uthash b/gtk-server/uthash index 41c357f..6d85739 160000 --- a/gtk-server/uthash +++ b/gtk-server/uthash @@ -1 +1 @@ -Subproject commit 41c357fd74ade4f4b4822c4407d2f51c4558e18d +Subproject commit 6d8573997c21f24c7e4ec9e48734b44f384170a1 diff --git a/llama/llama.cpp b/llama/llama.cpp index 2635ac7..58e68df 160000 --- a/llama/llama.cpp +++ b/llama/llama.cpp @@ -1 +1 @@ -Subproject commit 2635ac76e8aeec35ca8e71af70eb838d99df1510 +Subproject commit 58e68df0f91dd16ff56423ee5ef44062ed73bdfc diff --git a/nuklear/Nuklear b/nuklear/Nuklear index c98aa92..5a54a9f 160000 --- a/nuklear/Nuklear +++ b/nuklear/Nuklear @@ -1 +1 @@ -Subproject commit c98aa9247bb2354a2afc126f59d5fc6a45fe3c73 +Subproject commit 5a54a9f677ead97a581b5c8ab83cc30fdf237885 diff --git a/raylib/README.md b/raylib/README.md index 08f508e..070e8a1 100644 --- a/raylib/README.md +++ b/raylib/README.md @@ -1,16 +1,16 @@ -*Raylib* _MAJOR 5 _MINOR 6 _PATCH 0 5.6-dev +*Raylib* _MAJOR 6 _MINOR 1 _PATCH 0 6.1-dev ======= raylib is a simple and easy-to-use library to enjoy videogames programming. https://www.raylib.com/ -Implemented APIs (646) +Implemented APIs (651) ---------------- | Name | Description | |---------|---------------| | sub BeginBlendMode(mode) | Begin blending mode (alpha, additive, multiplied, subtract, custom) | -| sub BeginDrawing() | Setup canvas (framebuffer) to start drawing | +| sub BeginDrawing() | Begin canvas (framebuffer) drawing | | sub BeginMode2D(camera) | Begin 2D mode with custom camera (2D) | | sub BeginMode3D(camera) | Begin 3D mode with custom camera (3D) | | sub BeginScissorMode(x, y, width, height) | Begin scissor mode (define screen area for following drawing) | @@ -19,7 +19,7 @@ Implemented APIs (646) | func ChangeDirectory(dirPath) | Change working directory, return true on success | | func CheckCollisionBoxes(box1, box2) | Check collision between two bounding boxes | | func CheckCollisionBoxSphere(box, center, radius) | Check collision between box and sphere | -| func CheckCollisionCircleLine(center, radius, p1, p2) | Check if circle collides with a line created betweeen two points [p1] and [p2] | +| func CheckCollisionCircleLine(center, radius, p1, p2) | Check if circle collides with a line created between two points [p1] and [p2] | | func CheckCollisionCircleRec(center, radius, rec) | Check collision between circle and rectangle | | func CheckCollisionCircles(center1, radius1, center2, radius2) | Check collision between two circles | | func CheckCollisionLines(startPos1, endPos1, startPos2, endPos2, collisionPoint) | Check the collision between two lines defined by two points each, returns collision point by reference | @@ -30,7 +30,7 @@ Implemented APIs (646) | func CheckCollisionPointTriangle(point, p1, p2, p3) | Check if point is inside a triangle | | func CheckCollisionRecs(rec1, rec2) | Check collision between two rectangles | | func CheckCollisionSpheres(center1, radius1, center2, radius2) | Check collision between two spheres | -| sub ClearBackground(color) | Set background color (framebuffer clear color) | +| sub ClearBackground(color) | Clear background (framebuffer) to color | | sub ClearWindowState(flags) | Clear window configuration state flags | | sub CloseAudioDevice() | Close the audio device and context | | func closePhysics() | n/a | @@ -60,17 +60,17 @@ Implemented APIs (646) | func DecompressData(compData, compDataSize, dataSize) | Decompress data (DEFLATE algorithm), memory must be MemFree() | | func destroyPhysicsbody() | n/a | | func DirectoryExists(dirPath) | Check if a directory path exists | -| sub DisableCursor() | Disables cursor (lock cursor) | +| sub DisableCursor() | Disable cursor (lock cursor) | | sub DisableEventWaiting() | Disable waiting for events on EndDrawing(), automatic events polling | | sub DrawBillboard(camera, texture, position, scale, tint) | Draw a billboard texture | | sub DrawBillboardPro(camera, texture, source, position, up, size, origin, rotation, tint) | Draw a billboard texture defined by source and rotation | | sub DrawBillboardRec(camera, texture, source, position, size, tint) | Draw a billboard texture defined by source | | sub DrawBoundingBox(box, color) | Draw bounding box (wires) | -| sub DrawCapsule(startPos, endPos, radius, slices, rings, color) | Draw a capsule with the center of its sphere caps at startPos and endPos | -| sub DrawCapsuleWires(startPos, endPos, radius, slices, rings, color) | Draw capsule wireframe with the center of its sphere caps at startPos and endPos | +| sub DrawCapsule(startPos, endPos, radius, rings, slices, color) | Draw a capsule with the center of its sphere caps at startPos and endPos | +| sub DrawCapsuleWires(startPos, endPos, radius, rings, slices, color) | Draw capsule wireframe with the center of its sphere caps at startPos and endPos | | sub DrawCircle(centerX, centerY, radius, color) | Draw a color-filled circle | | sub DrawCircle3D(center, radius, rotationAxis, rotationAngle, color) | Draw a circle in 3D world space | -| sub DrawCircleGradient(centerX, centerY, radius, inner, outer) | Draw a gradient-filled circle | +| sub DrawCircleGradient(center, radius, inner, outer) | Draw a gradient-filled circle | | sub DrawCircleLines(centerX, centerY, radius, color) | Draw circle outline | | sub DrawCircleLinesV(center, radius, color) | Draw circle outline (Vector version) | | sub DrawCircleSector(center, radius, startAngle, endAngle, segments, color) | Draw a piece of a circle | @@ -83,7 +83,7 @@ Implemented APIs (646) | sub DrawCylinder(position, radiusTop, radiusBottom, height, slices, color) | Draw a cylinder/cone | | sub DrawCylinderEx(startPos, endPos, startRadius, endRadius, sides, color) | Draw a cylinder with base at startPos and top at endPos | | sub DrawCylinderWires(position, radiusTop, radiusBottom, height, slices, color) | Draw a cylinder/cone wires | -| sub DrawCylinderWiresEx(startPos, endPos, startRadius, endRadius, sides, color) | Draw a cylinder wires with base at startPos and top at endPos | +| sub DrawCylinderWiresEx(startPos, endPos, startRadius, endRadius, slices, color) | Draw a cylinder wires with base at startPos and top at endPos | | sub DrawEllipse(centerX, centerY, radiusH, radiusV, color) | Draw ellipse | | sub DrawEllipseLines(centerX, centerY, radiusH, radiusV, color) | Draw ellipse outline | | sub DrawEllipseLinesV(center, radiusH, radiusV, color) | Draw ellipse outline (Vector version) | @@ -99,15 +99,13 @@ Implemented APIs (646) | sub DrawLineV(startPos, endPos, color) | Draw a line (using gl lines) | | sub DrawModel(model, position, scale, tint) | Draw a model (with texture if set) | | sub DrawModelEx(model, position, rotationAxis, rotationAngle, scale, tint) | Draw a model with extended parameters | -| sub DrawModelPoints(model, position, scale, tint) | Draw a model as points | -| sub DrawModelPointsEx(model, position, rotationAxis, rotationAngle, scale, tint) | Draw a model as points with extended parameters | | sub DrawModelWires(model, position, scale, tint) | Draw a model wires (with texture if set) | | sub DrawModelWiresEx(model, position, rotationAxis, rotationAngle, scale, tint) | Draw a model wires (with texture if set) with extended parameters | | sub DrawPixel(posX, posY, color) | Draw a pixel using geometry [Can be slow, use with care] | | sub DrawPixelV(position, color) | Draw a pixel using geometry (Vector version) [Can be slow, use with care] | | sub DrawPlane(centerPos, size, color) | Draw a plane XZ | | sub DrawPoint3D(position, color) | Draw a point in 3D space, actually a small line | -| sub DrawPoly(center, sides, radius, rotation, color) | Draw a regular polygon (Vector version) | +| sub DrawPoly(center, sides, radius, rotation, color) | Draw a polygon of n sides | | sub DrawPolyLines(center, sides, radius, rotation, color) | Draw a polygon outline of n sides | | sub DrawPolyLinesEx(center, sides, radius, rotation, lineThick, color) | Draw a polygon outline of n sides with extended parameters | | sub DrawRay(ray, color) | Draw a ray line | @@ -121,7 +119,7 @@ Implemented APIs (646) | sub DrawRectangleRec(rec, color) | Draw a color-filled rectangle | | sub DrawRectangleRounded(rec, roundness, segments, color) | Draw rectangle with rounded edges | | sub DrawRectangleRoundedLines(rec, roundness, segments, color) | Draw rectangle lines with rounded edges | -| sub DrawRectangleRoundedLinesEx(rec, roundness, segments, lineThick, color) | Draw rectangle with rounded edges outline | +| sub DrawRectangleRoundedLinesEx(rec, roundness, segments, lineThick, color) | Draw rectangle lines with rounded edges outline | | sub DrawRectangleV(position, size, color) | Draw a color-filled rectangle (Vector version) | | sub DrawRing(center, innerRadius, outerRadius, startAngle, endAngle, segments, color) | Draw ring | | sub DrawRingLines(center, innerRadius, outerRadius, startAngle, endAngle, segments, color) | Draw ring outline | @@ -140,38 +138,39 @@ Implemented APIs (646) | sub DrawSplineSegmentLinear(p1, p2, thick, color) | Draw spline segment: Linear, 2 points | | sub DrawText(text, posX, posY, fontSize, color) | Draw text (using default font) | | sub DrawTextCodepoint(font, codepoint, position, fontSize, tint) | Draw one character (codepoint) | -| sub DrawTextCodepoints(font, codepoints, codepointCount, position, fontSize, spacing, tint) | Draw multiple character (codepoint) | +| sub DrawTextCodepoints(font, codepoints, codepointCount, position, fontSize, spacing, tint) | Draw multiple characters (codepoint) | | sub DrawTextEx(font, text, position, fontSize, spacing, tint) | Draw text using font and additional parameters | | sub DrawTextPro(font, text, position, origin, rotation, fontSize, spacing, tint) | Draw text using Font and pro parameters (rotation) | | sub DrawTexture(texture, posX, posY, tint) | Draw a Texture2D | | sub DrawTextureEx(texture, position, rotation, scale, tint) | Draw a Texture2D with extended parameters | -| sub DrawTextureNPatch(texture, nPatchInfo, dest, origin, rotation, tint) | Draws a texture (or part of it) that stretches or shrinks nicely | +| sub DrawTextureNPatch(texture, nPatchInfo, dest, origin, rotation, tint) | Draw a texture (or part of it) that stretches or shrinks nicely | | sub DrawTexturePro(texture, source, dest, origin, rotation, tint) | Draw a part of a texture defined by a rectangle with 'pro' parameters | | sub DrawTextureRec(texture, source, position, tint) | Draw a part of a texture defined by a rectangle | | sub DrawTextureV(texture, position, tint) | Draw a Texture2D with position defined as Vector2 | | sub DrawTriangle(v1, v2, v3, color) | Draw a color-filled triangle (vertex in counter-clockwise order!) | | sub DrawTriangle3D(v1, v2, v3, color) | Draw a color-filled triangle (vertex in counter-clockwise order!) | | sub DrawTriangleFan(points, pointCount, color) | Draw a triangle fan defined by points (first vertex is the center) | +| sub DrawTriangleGradient(v1, v2, v3, c1, c2, c3) | Draw triangle with interpolated colors (vertex in counter-clockwise order!) | | sub DrawTriangleLines(v1, v2, v3, color) | Draw triangle outline (vertex in counter-clockwise order!) | | sub DrawTriangleStrip(points, pointCount, color) | Draw a triangle strip defined by points | | sub DrawTriangleStrip3D(points, pointCount, color) | Draw a triangle strip defined by points | -| sub EnableCursor() | Enables cursor (unlock cursor) | +| sub EnableCursor() | Enable cursor (unlock cursor) | | sub EnableEventWaiting() | Enable waiting for events on EndDrawing(), no automatic event polling | | func EncodeDataBase64(data, dataSize, outputSize) | Encode data to Base64 string (includes NULL terminator), memory must be MemFree() | | sub EndBlendMode() | End blending mode (reset to default: alpha blending) | -| sub EndDrawing() | End canvas drawing and swap buffers (double buffering) | -| sub EndMode2D() | Ends 2D mode with custom camera | -| sub EndMode3D() | Ends 3D mode and returns to default 2D orthographic mode | +| sub EndDrawing() | End canvas (framebuffer) drawing and swap buffers (double buffering) | +| sub EndMode2D() | End 2D mode with custom camera | +| sub EndMode3D() | End 3D mode and returns to default 2D orthographic mode | | sub EndScissorMode() | End scissor mode | | sub EndShaderMode() | End custom shader drawing (use default shader) | -| sub EndTextureMode() | Ends drawing to render texture | +| sub EndTextureMode() | End drawing to render texture | | sub EndVrStereoMode() | End stereo rendering (requires VR simulator) | | func ExportAutomationEventList(list, fileName) | Export automation events list as text file | | func ExportDataAsCode(data, dataSize, fileName) | Export data to code (.h), returns true on success | | func ExportFontAsCode(font, fileName) | Export font as code file, returns true on success | | func ExportImage(image, fileName) | Export image data to file, returns true on success | | func ExportImageAsCode(image, fileName) | Export image as code file defining an array of bytes, returns true on success | -| func ExportImageToMemory(image, fileType, fileSize) | Export image to memory buffer | +| func ExportImageToMemory(image, fileType, fileSize) | Export image to memory buffer, memory must be MemFree() | | func ExportMesh(mesh, fileName) | Export mesh data to file, returns true on success | | func ExportMeshAsCode(mesh, fileName) | Export mesh as code file (.h) defining multiple arrays of vertex attributes | | func ExportWave(wave, fileName) | Export wave data to file, returns true on success | @@ -219,6 +218,8 @@ Implemented APIs (646) | func GetCollisionRec(rec1, rec2) | Get collision rectangle for two rectangles collision | | func GetColor(hexValue) | Get Color structure from hexadecimal value | | func GetCurrentMonitor() | Get current monitor where window is placed | +| func GetDirectoryFileCount(dirPath) | Get the file count in a directory | +| func GetDirectoryFileCountEx(basePath, filter, scanSubdirs) | Get the file count in a directory with extension filtering and recursive directory scan. Use 'DIR' in the filter string to include directories in the result | | func GetDirectoryPath(filePath) | Get full path for a given fileName with path (uses static string) | | func GetFileExtension(fileName) | Get pointer to extension for a filename string (includes dot: '.png') | | func GetFileLength(fileName) | Get file length in bytes (NOTE: GetFileSize() conflicts with windows.h) | @@ -279,7 +280,7 @@ Implemented APIs (646) | func GetRenderHeight() | Get current render height (it considers HiDPI) | | func GetRenderWidth() | Get current render width (it considers HiDPI) | | func GetScreenHeight() | Get current screen height | -| func GetScreenToWorld2D(position, camera) | Get the world space position for a 2d camera screen space position | +| func GetScreenToWorld2D(position, camera) | Get world space position for a 2d camera screen space position | | func GetScreenToWorldRay(position, camera) | Get a ray trace from screen position (i.e mouse) | | func GetScreenToWorldRayEx(position, camera, width, height) | Get a ray trace from screen position (i.e mouse) in a viewport | | func GetScreenWidth() | Get current screen width | @@ -289,7 +290,7 @@ Implemented APIs (646) | func GetShapesTextureRectangle() | Get texture source rectangle that is used for shapes drawing | | func GetSplinePointBasis(p1, p2, p3, p4, t) | Get (evaluate) spline point: B-Spline | | func GetSplinePointBezierCubic(p1, c2, c3, p4, t) | Get (evaluate) spline point: Cubic Bezier | -| func GetSplinePointBezierQuad(p1, c2, p3, t) | Get (evaluate) spline point: Quadratic Bezier | +| func GetSplinePointBezierQuadratic(p1, c2, p3, t) | Get (evaluate) spline point: Quadratic Bezier | | func GetSplinePointCatmullRom(p1, p2, p3, p4, t) | Get (evaluate) spline point: Catmull-Rom | | func GetSplinePointLinear(startPos, endPos, t) | Get (evaluate) spline point: Linear | | func GetTextBetween(text, begin, end) | Get text between two strings | @@ -303,9 +304,9 @@ Implemented APIs (646) | func GetWindowPosition() | Get window position XY on monitor | | func GetWindowScaleDPI() | Get window scale DPI factor | | func GetWorkingDirectory() | Get current working directory (uses static string) | -| func GetWorldToScreen(position, camera) | Get the screen space position for a 3d world space position | -| func GetWorldToScreen2D(position, camera) | Get the screen space position for a 2d camera world space position | -| func GetWorldToScreenEx(position, camera, width, height) | Get size position for a 3d world space position | +| func GetWorldToScreen(position, camera) | Get screen space position for a 3d world space position | +| func GetWorldToScreen2D(position, camera) | Get screen space position for a 2d camera world space position | +| func GetWorldToScreenEx(position, camera, width, height) | Get sized screen space position for a 3d world space position | | func guibutton() | n/a | | func guicheckbox() | n/a | | func guicolorbaralpha() | n/a | @@ -344,7 +345,7 @@ Implemented APIs (646) | func guiunlock() | n/a | | func guivaluebox() | n/a | | func guiwindowbox() | n/a | -| sub HideCursor() | Hides cursor | +| sub HideCursor() | Hide cursor | | sub ImageAlphaClear(image, color, threshold) | Clear alpha channel to desired color | | sub ImageAlphaCrop(image, threshold) | Crop image depending on alpha value | | sub ImageAlphaMask(image, alphaMask) | Apply alpha mask to image | @@ -371,14 +372,15 @@ Implemented APIs (646) | sub ImageDrawPixel(dst, posX, posY, color) | Draw pixel within an image | | sub ImageDrawPixelV(dst, position, color) | Draw pixel within an image (Vector version) | | sub ImageDrawRectangle(dst, posX, posY, width, height, color) | Draw rectangle within an image | -| sub ImageDrawRectangleLines(dst, rec, thick, color) | Draw rectangle lines within an image | +| sub ImageDrawRectangleLines(dst, posX, posY, width, height, color) | Draw rectangle lines within an image | +| sub ImageDrawRectangleLinesEx(dst, rec, thick, color) | Draw rectangle lines within an image with extended parameters | | sub ImageDrawRectangleRec(dst, rec, color) | Draw rectangle within an image | | sub ImageDrawRectangleV(dst, position, size, color) | Draw rectangle within an image (Vector version) | | sub ImageDrawText(dst, text, posX, posY, fontSize, color) | Draw text (using default font) within an image (destination) | | sub ImageDrawTextEx(dst, font, text, position, fontSize, spacing, tint) | Draw text (custom sprite font) within an image (destination) | | sub ImageDrawTriangle(dst, v1, v2, v3, color) | Draw triangle within an image | -| sub ImageDrawTriangleEx(dst, v1, v2, v3, c1, c2, c3) | Draw triangle with interpolated colors within an image | | sub ImageDrawTriangleFan(dst, points, pointCount, color) | Draw a triangle fan defined by points within an image (first vertex is the center) | +| sub ImageDrawTriangleGradient(dst, v1, v2, v3, c1, c2, c3) | Draw triangle with interpolated colors within an image | | sub ImageDrawTriangleLines(dst, v1, v2, v3, color) | Draw triangle outline within an image | | sub ImageDrawTriangleStrip(dst, points, pointCount, color) | Draw a triangle strip defined by points within an image | | sub ImageFlipHorizontal(image) | Flip image horizontally | @@ -403,7 +405,7 @@ Implemented APIs (646) | func IsAudioDeviceReady() | Check if audio device has been initialized successfully | | func IsAudioStreamPlaying(stream) | Check if audio stream is playing | | func IsAudioStreamProcessed(stream) | Check if any audio stream buffers requires refill | -| func IsAudioStreamValid(stream) | Checks if an audio stream is valid (buffers initialized) | +| func IsAudioStreamValid(stream) | Check if an audio stream is valid (buffers initialized) | | func IsCursorHidden() | Check if cursor is not visible | | func IsCursorOnScreen() | Check if cursor is on the screen | | func IsFileDropped() | Check if a file has been dropped into window | @@ -415,7 +417,7 @@ Implemented APIs (646) | func IsGamepadButtonPressed(gamepad, button) | Check if a gamepad button has been pressed once | | func IsGamepadButtonReleased(gamepad, button) | Check if a gamepad button has been released once | | func IsGamepadButtonUp(gamepad, button) | Check if a gamepad button is NOT being pressed | -| func IsGestureDetected(gesture) | Check if a gesture have been detected | +| func IsGestureDetected(gesture) | Check if a gesture has been detected | | func IsImageValid(image) | Check if an image is valid (data and parameters) | | func IsKeyDown(key) | Check if a key is being pressed | | func IsKeyPressed(key) | Check if a key has been pressed once | @@ -429,14 +431,14 @@ Implemented APIs (646) | func IsMouseButtonReleased(button) | Check if a mouse button has been released once | | func IsMouseButtonUp(button) | Check if a mouse button is NOT being pressed | | func IsMusicStreamPlaying(music) | Check if music is playing | -| func IsMusicValid(music) | Checks if a music stream is valid (context and buffers initialized) | +| func IsMusicValid(music) | Check if a music stream is valid (context and buffers initialized) | | func IsPathFile(path) | Check if a given path is a file or a directory | | func IsRenderTextureValid(target) | Check if a render texture is valid (loaded in GPU) | | func IsShaderValid(shader) | Check if a shader is valid (loaded on GPU) | | func IsSoundPlaying(sound) | Check if a sound is currently playing | -| func IsSoundValid(sound) | Checks if a sound is valid (data loaded and buffers initialized) | +| func IsSoundValid(sound) | Check if a sound is valid (data loaded and buffers initialized) | | func IsTextureValid(texture) | Check if a texture is valid (loaded in GPU) | -| func IsWaveValid(wave) | Checks if wave data is valid (data loaded and parameters) | +| func IsWaveValid(wave) | Check if wave data is valid (data loaded and parameters) | | func IsWindowFocused() | Check if window is currently focused | | func IsWindowFullscreen() | Check if window is currently fullscreen | | func IsWindowHidden() | Check if window is currently hidden | @@ -448,8 +450,8 @@ Implemented APIs (646) | func LoadAudioStream(sampleRate, sampleSize, channels) | Load audio stream (to stream raw audio pcm data) | | func LoadAutomationEventList(fileName) | Load automation events list from file, NULL for empty list, capacity = MAX_AUTOMATION_EVENTS | | func LoadCodepoints(text, count) | Load all codepoints from a UTF-8 text string, codepoints count returned by parameter | -| func LoadDirectoryFiles(dirPath) | Load directory filepaths | -| func LoadDirectoryFilesEx(basePath, filter, scanSubdirs) | Load directory filepaths with extension filtering and recursive directory scan. Use 'DIR' in the filter string to include directories in the result | +| func LoadDirectoryFiles(dirPath) | Load directory filepaths, files and directories, no subdirs scan | +| func LoadDirectoryFilesEx(basePath, filter, scanSubdirs) | Load directory filepaths with extension filtering and subdir scan; some filters available: `*.*`,`FILES*`,`DIRS*` | | func LoadDroppedFiles() | Load dropped filepaths | | func LoadFileData(fileName, dataSize) | Load file data as byte array (read) | | func LoadFileText(fileName) | Load text data from file (read), returns a '\\0' terminated string | @@ -462,7 +464,7 @@ Implemented APIs (646) | func LoadImageAnimFromMemory(fileType, fileData, dataSize, frames) | Load image sequence from memory buffer | | func LoadImageColors(image) | Load color data from image as a Color array (RGBA - 32bit) | | func LoadImageFromMemory(fileType, fileData, dataSize) | Load image from memory buffer, fileType refers to extension: i.e. '.png' | -| func LoadImageFromScreen() | Load image from screen buffer and (screenshot) | +| func LoadImageFromScreen() | Load image from screen buffer (screenshot) | | func LoadImageFromTexture(texture) | Load image from GPU texture data | | func LoadImagePalette(image, maxPaletteSize, colorCount) | Load colors palette from image as a Color array (RGBA - 32bit) | | func LoadImageRaw(fileName, width, height, format, headerSize) | Load image from RAW file data | @@ -476,7 +478,7 @@ Implemented APIs (646) | func LoadShader(vsFileName, fsFileName) | Load shader from files and bind default locations | | func LoadShaderFromMemory(vsCode, fsCode) | Load shader from code strings and bind default locations | | func LoadSound(fileName) | Load sound from file | -| func LoadSoundAlias(source) | Create a new sound that shares the same sample data as the source sound, does not own the sound data | +| func LoadSoundAlias(source) | Load sound alias, new sound that shares the same sample data as the source sound, does not own the sound data | | func LoadSoundFromWave(wave) | Load sound from wave data | | func LoadTexture(fileName) | Load texture from file into GPU memory (VRAM) | | func LoadTextureCubemap(image, layout) | Load cubemap from image, multiple image cubemap layouts supported | @@ -488,6 +490,7 @@ Implemented APIs (646) | func MakeDirectory(dirPath) | Create directories (including full path requested), returns 0 on success | | sub MaximizeWindow() | Set window state: maximized, if resizable | | func MeasureText(text, fontSize) | Measure string width for default font | +| func MeasureTextCodepoints(font, codepoints, length, fontSize, spacing) | Measure string size for an existing array of codepoints for Font | | func MeasureTextEx(font, text, fontSize, spacing) | Measure string size for Font | | func MemAlloc(size) | Internal memory allocator | | sub MemFree(ptr) | Internal memory free | @@ -517,13 +520,13 @@ Implemented APIs (646) | func SaveFileText(fileName, text) | Save text data to file (write), string must be '\\0' terminated, returns true on success | | sub SeekMusicStream(music, position) | Seek music to a position (in seconds) | | sub SetAudioStreamBufferSizeDefault(size) | Default size for new audio streams | -| sub SetAudioStreamPan(stream, pan) | Set pan for audio stream (0.5 is centered) | +| sub SetAudioStreamPan(stream, pan) | Set pan for audio stream (-1.0 left, 0.0 center, 1.0 right) | | sub SetAudioStreamPitch(stream, pitch) | Set pitch for audio stream (1.0 is base level) | | sub SetAudioStreamVolume(stream, volume) | Set volume for audio stream (1.0 is max level) | | sub SetAutomationEventBaseFrame(frame) | Set automation event internal base frame to start recording | | sub SetAutomationEventList(list) | Set automation event list to record to | | sub SetClipboardText(text) | Set clipboard text content | -| sub SetConfigFlags(flags) | Setup init configuration flags (view FLAGS) | +| sub SetConfigFlags(flags) | Set up init configuration flags (view FLAGS) | | sub SetExitKey(key) | Set a custom key to exit program (default is ESC) | | func SetGamepadMappings(mappings) | Set internal gamepad mappings (SDL_GameControllerDB) | | sub SetGamepadVibration(gamepad, leftMotor, rightMotor, duration) | Set gamepad vibration for both motors (duration in seconds) | @@ -536,8 +539,8 @@ Implemented APIs (646) | sub SetMouseOffset(offsetX, offsetY) | Set mouse offset | | sub SetMousePosition(x, y) | Set mouse position XY | | sub SetMouseScale(scaleX, scaleY) | Set mouse scaling | -| sub SetMusicPan(music, pan) | Set pan for a music (-1.0 left, 0.0 center, 1.0 right) | -| sub SetMusicPitch(music, pitch) | Set pitch for a music (1.0 is base level) | +| sub SetMusicPan(music, pan) | Set pan for music (-1.0 left, 0.0 center, 1.0 right) | +| sub SetMusicPitch(music, pitch) | Set pitch for music (1.0 is base level) | | sub SetMusicVolume(music, volume) | Set volume for music (1.0 is max level) | | func setPhysicsbodyangularvelocity() | n/a | | func setPhysicsbodydynamicfriction() | n/a | @@ -585,7 +588,7 @@ Implemented APIs (646) | sub SetWindowSize(width, height) | Set window dimensions | | sub SetWindowState(flags) | Set window configuration state using flags | | sub SetWindowTitle(title) | Set title for window | -| sub ShowCursor() | Shows cursor | +| sub ShowCursor() | Show cursor | | sub StartAutomationEventRecording() | Start recording automation events (AutomationEventList must be set) | | sub StopAudioStream(stream) | Stop audio stream | | sub StopAutomationEventRecording() | Stop recording automation events | @@ -597,12 +600,15 @@ Implemented APIs (646) | func TextCopy(dst, src) | Copy one string to another, returns bytes copied | | func TextFindIndex(text, search) | Find first text occurrence within a string, -1 if not found | | func TextFormat(text, args) | Text formatting with variables (sprintf() style) | -| func TextInsert(text, insert, position) | Insert text in a position (WARNING: memory must be freed!) | -| func TextIsEqual(text1, text2) | Check if two text string are equal | +| func TextInsert(text, insert, position) | Insert text in a defined byte position | +| func TextInsertAlloc(text, insert, position) | Insert text in a defined byte position, memory must be MemFree() | +| func TextIsEqual(text1, text2) | Check if two text strings are equal | | func TextLength(text) | Get text length, checks for '\\0' ending | | func TextRemoveSpaces(text) | Remove text spaces, concat words | -| func TextReplace(text, search, replacement) | Replace text string (WARNING: memory must be freed!) | -| func TextReplaceBetween(text, begin, end, replacement) | Replace text between two specific strings (WARNING: memory must be freed!) | +| func TextReplace(text, search, replacement) | Replace text string with new string | +| func TextReplaceAlloc(text, search, replacement) | Replace text string with new string, memory must be MemFree() | +| func TextReplaceBetween(text, begin, end, replacement) | Replace text between two specific strings | +| func TextReplaceBetweenAlloc(text, begin, end, replacement) | Replace text between two specific strings, memory must be MemFree() | | func TextSubtext(text, position, length) | Get a piece of a text string | | func TextToCamel(text) | Get Camel case notation version of provided string | | func TextToFloat(text) | Get float value from text | @@ -625,14 +631,13 @@ Implemented APIs (646) | sub UnloadImagePalette(colors) | Unload colors palette loaded with LoadImagePalette() | | sub UnloadMesh(mesh) | Unload mesh data from CPU and GPU | | sub UnloadModel(model) | Unload model (including meshes) from memory (RAM and/or VRAM) | -| sub UnloadModelAnimation(anim) | Unload animation data | | sub UnloadModelAnimations(animations, animCount) | Unload animation array data | | sub UnloadMusicStream(music) | Unload music stream | | sub UnloadRandomSequence(sequence) | Unload random values sequence | | sub UnloadRenderTexture(target) | Unload render texture from GPU memory (VRAM) | | sub UnloadShader(shader) | Unload shader from GPU memory (VRAM) | | sub UnloadSound(sound) | Unload sound | -| sub UnloadSoundAlias(alias) | Unload a sound alias (does not deallocate sample data) | +| sub UnloadSoundAlias(alias) | Unload sound alias (does not deallocate sample data) | | sub UnloadTexture(texture) | Unload texture from GPU memory (VRAM) | | sub UnloadUTF8(text) | Unload UTF-8 text encoded from codepoints array | | sub UnloadWave(wave) | Unload wave data | @@ -641,11 +646,11 @@ Implemented APIs (646) | func updateautomationeventlist() | n/a | | sub UpdateCamera(camera, mode) | Update camera position for selected mode | | sub UpdateMeshBuffer(mesh, index, data, dataSize, offset) | Update mesh vertex data in GPU for a specific buffer index | -| sub UpdateModelAnimation(model, anim, frame) | Update model animation pose (CPU) | -| sub UpdateModelAnimationBones(model, anim, frame) | Update model animation mesh bone matrices (GPU skinning) | -| sub UpdateMusicStream(music) | Updates buffers for music streaming | +| sub UpdateModelAnimation(model, anim, frame) | Update model animation pose (vertex buffers and bone matrices) | +| sub UpdateModelAnimationEx(model, animA, frameA, animB, frameB, blend) | Update model animation pose, blending two animations | +| sub UpdateMusicStream(music) | Update buffers for music streaming | | func updatePhysics() | n/a | -| sub UpdateSound(sound, data, sampleCount) | Update sound buffer with new data (default data format: 32 bit float, stereo) | +| sub UpdateSound(sound, data, frameCount) | Update sound buffer with new data (default data format: 32 bit float, stereo) | | sub UpdateTexture(texture, pixels) | Update GPU texture with new data (pixels should be able to fill texture) | | sub UpdateTextureRec(texture, rec, pixels) | Update GPU texture rectangle with new data (pixels and rec should fit in texture) | | sub UploadMesh(mesh, dynamic) | Upload mesh vertex data in GPU and provide VAO/VBO ids | diff --git a/raylib/func-def.h b/raylib/func-def.h index 42552b2..a939104 100644 --- a/raylib/func-def.h +++ b/raylib/func-def.h @@ -85,6 +85,8 @@ {2, 2, "GETCOLLISIONREC", cmd_getcollisionrec}, {1, 1, "GETCOLOR", cmd_getcolor}, {0, 0, "GETCURRENTMONITOR", cmd_getcurrentmonitor}, + {1, 1, "GETDIRECTORYFILECOUNT", cmd_getdirectoryfilecount}, + {3, 3, "GETDIRECTORYFILECOUNTEX", cmd_getdirectoryfilecountex}, {1, 1, "GETDIRECTORYPATH", cmd_getdirectorypath}, {1, 1, "GETFILEEXTENSION", cmd_getfileextension}, {1, 1, "GETFILELENGTH", cmd_getfilelength}, @@ -150,7 +152,7 @@ {0, 0, "GETSHAPESTEXTURERECTANGLE", cmd_getshapestexturerectangle}, {5, 5, "GETSPLINEPOINTBASIS", cmd_getsplinepointbasis}, {5, 5, "GETSPLINEPOINTBEZIERCUBIC", cmd_getsplinepointbeziercubic}, - {4, 4, "GETSPLINEPOINTBEZIERQUAD", cmd_getsplinepointbezierquad}, + {4, 4, "GETSPLINEPOINTBEZIERQUADRATIC", cmd_getsplinepointbezierquadratic}, {5, 5, "GETSPLINEPOINTCATMULLROM", cmd_getsplinepointcatmullrom}, {3, 3, "GETSPLINEPOINTLINEAR", cmd_getsplinepointlinear}, {3, 3, "GETTEXTBETWEEN", cmd_gettextbetween}, @@ -256,6 +258,7 @@ {1, 1, "LOADWAVESAMPLES", cmd_loadwavesamples}, {1, 1, "MAKEDIRECTORY", cmd_makedirectory}, {2, 2, "MEASURETEXT", cmd_measuretext}, + {5, 5, "MEASURETEXTCODEPOINTS", cmd_measuretextcodepoints}, {4, 4, "MEASURETEXTEX", cmd_measuretextex}, {1, 1, "MEMALLOC", cmd_memalloc}, {2, 2, "MEMREALLOC", cmd_memrealloc}, @@ -265,11 +268,14 @@ {2, 2, "TEXTCOPY", cmd_textcopy}, {2, 2, "TEXTFINDINDEX", cmd_textfindindex}, {3, 3, "TEXTINSERT", cmd_textinsert}, + {3, 3, "TEXTINSERTALLOC", cmd_textinsertalloc}, {2, 2, "TEXTISEQUAL", cmd_textisequal}, {1, 1, "TEXTLENGTH", cmd_textlength}, {1, 1, "TEXTREMOVESPACES", cmd_textremovespaces}, {3, 3, "TEXTREPLACE", cmd_textreplace}, + {3, 3, "TEXTREPLACEALLOC", cmd_textreplacealloc}, {4, 4, "TEXTREPLACEBETWEEN", cmd_textreplacebetween}, + {4, 4, "TEXTREPLACEBETWEENALLOC", cmd_textreplacebetweenalloc}, {3, 3, "TEXTSUBTEXT", cmd_textsubtext}, {1, 1, "TEXTTOCAMEL", cmd_texttocamel}, {1, 1, "TEXTTOFLOAT", cmd_texttofloat}, diff --git a/raylib/func.h b/raylib/func.h index 53ccee6..441fc4d 100644 --- a/raylib/func.h +++ b/raylib/func.h @@ -32,7 +32,7 @@ static int cmd_checkcollisionboxsphere(int argc, slib_par_t *params, var_t *retv } // -// Check if circle collides with a line created betweeen two points [p1] and [p2] +// Check if circle collides with a line created between two points [p1] and [p2] // static int cmd_checkcollisioncircleline(int argc, slib_par_t *params, var_t *retval) { auto center = get_param_vec2(argc, params, 0); @@ -328,7 +328,7 @@ static int cmd_compressdata(int argc, slib_par_t *params, var_t *retval) { // Compute CRC32 hash code // static int cmd_computecrc32(int argc, slib_par_t *params, var_t *retval) { - auto data = (unsigned char *)get_param_str(argc, params, 0, 0); + auto data = (const unsigned char *)get_param_str(argc, params, 0, 0); auto dataSize = get_param_int(argc, params, 1, 0); auto fnResult = ComputeCRC32(data, dataSize); v_setint(retval, fnResult); @@ -339,7 +339,7 @@ static int cmd_computecrc32(int argc, slib_par_t *params, var_t *retval) { // Compute MD5 hash code, returns static int[4] (16 bytes) // static int cmd_computemd5(int argc, slib_par_t *params, var_t *retval) { - auto data = (unsigned char *)get_param_str(argc, params, 0, 0); + auto data = (const unsigned char *)get_param_str(argc, params, 0, 0); auto dataSize = get_param_int(argc, params, 1, 0); auto fnResult = (var_int_t)ComputeMD5(data, dataSize); v_setint(retval, fnResult); @@ -350,7 +350,7 @@ static int cmd_computemd5(int argc, slib_par_t *params, var_t *retval) { // Compute SHA1 hash code, returns static int[5] (20 bytes) // static int cmd_computesha1(int argc, slib_par_t *params, var_t *retval) { - auto data = (unsigned char *)get_param_str(argc, params, 0, 0); + auto data = (const unsigned char *)get_param_str(argc, params, 0, 0); auto dataSize = get_param_int(argc, params, 1, 0); auto fnResult = (var_int_t)ComputeSHA1(data, dataSize); v_setint(retval, fnResult); @@ -361,7 +361,7 @@ static int cmd_computesha1(int argc, slib_par_t *params, var_t *retval) { // Compute SHA256 hash code, returns static int[8] (32 bytes) // static int cmd_computesha256(int argc, slib_par_t *params, var_t *retval) { - auto data = (unsigned char *)get_param_str(argc, params, 0, 0); + auto data = (const unsigned char *)get_param_str(argc, params, 0, 0); auto dataSize = get_param_int(argc, params, 1, 0); auto fnResult = (var_int_t)ComputeSHA256(data, dataSize); v_setint(retval, fnResult); @@ -497,7 +497,7 @@ static int cmd_exportimageascode(int argc, slib_par_t *params, var_t *retval) { } // -// Export image to memory buffer +// Export image to memory buffer, memory must be MemFree() // static int cmd_exportimagetomemory(int argc, slib_par_t *params, var_t *retval) { int result; @@ -1062,6 +1062,28 @@ static int cmd_getcurrentmonitor(int argc, slib_par_t *params, var_t *retval) { return 1; } +// +// Get the file count in a directory +// +static int cmd_getdirectoryfilecount(int argc, slib_par_t *params, var_t *retval) { + auto dirPath = get_param_str(argc, params, 0, 0); + auto fnResult = GetDirectoryFileCount(dirPath); + v_setint(retval, fnResult); + return 1; +} + +// +// Get the file count in a directory with extension filtering and recursive directory scan. Use 'DIR' in the filter string to include directories in the result +// +static int cmd_getdirectoryfilecountex(int argc, slib_par_t *params, var_t *retval) { + auto basePath = get_param_str(argc, params, 0, 0); + auto filter = get_param_str(argc, params, 1, 0); + auto scanSubdirs = get_param_int(argc, params, 2, 0); + auto fnResult = GetDirectoryFileCountEx(basePath, filter, scanSubdirs); + v_setint(retval, fnResult); + return 1; +} + // // Get full path for a given fileName with path (uses static string) // @@ -1661,7 +1683,7 @@ static int cmd_getscreenheight(int argc, slib_par_t *params, var_t *retval) { } // -// Get the world space position for a 2d camera screen space position +// Get world space position for a 2d camera screen space position // static int cmd_getscreentoworld2d(int argc, slib_par_t *params, var_t *retval) { auto position = get_param_vec2(argc, params, 0); @@ -1775,12 +1797,12 @@ static int cmd_getsplinepointbeziercubic(int argc, slib_par_t *params, var_t *re // // Get (evaluate) spline point: Quadratic Bezier // -static int cmd_getsplinepointbezierquad(int argc, slib_par_t *params, var_t *retval) { +static int cmd_getsplinepointbezierquadratic(int argc, slib_par_t *params, var_t *retval) { auto p1 = get_param_vec2(argc, params, 0); auto c2 = get_param_vec2(argc, params, 1); auto p3 = get_param_vec2(argc, params, 2); auto t = get_param_num(argc, params, 3, 0); - auto fnResult = GetSplinePointBezierQuad(p1, c2, p3, t); + auto fnResult = GetSplinePointBezierQuadratic(p1, c2, p3, t); v_setvec2(retval, fnResult); return 1; } @@ -1916,7 +1938,7 @@ static int cmd_getworkingdirectory(int argc, slib_par_t *params, var_t *retval) } // -// Get the screen space position for a 3d world space position +// Get screen space position for a 3d world space position // static int cmd_getworldtoscreen(int argc, slib_par_t *params, var_t *retval) { auto position = get_param_vec3(argc, params, 0); @@ -1927,7 +1949,7 @@ static int cmd_getworldtoscreen(int argc, slib_par_t *params, var_t *retval) { } // -// Get the screen space position for a 2d camera world space position +// Get screen space position for a 2d camera world space position // static int cmd_getworldtoscreen2d(int argc, slib_par_t *params, var_t *retval) { auto position = get_param_vec2(argc, params, 0); @@ -1938,7 +1960,7 @@ static int cmd_getworldtoscreen2d(int argc, slib_par_t *params, var_t *retval) { } // -// Get size position for a 3d world space position +// Get sized screen space position for a 3d world space position // static int cmd_getworldtoscreenex(int argc, slib_par_t *params, var_t *retval) { auto position = get_param_vec3(argc, params, 0); @@ -2074,7 +2096,7 @@ static int cmd_isaudiostreamprocessed(int argc, slib_par_t *params, var_t *retva } // -// Checks if an audio stream is valid (buffers initialized) +// Check if an audio stream is valid (buffers initialized) // static int cmd_isaudiostreamvalid(int argc, slib_par_t *params, var_t *retval) { int result; @@ -2208,7 +2230,7 @@ static int cmd_isgamepadbuttonup(int argc, slib_par_t *params, var_t *retval) { } // -// Check if a gesture have been detected +// Check if a gesture has been detected // static int cmd_isgesturedetected(int argc, slib_par_t *params, var_t *retval) { auto gesture = get_param_int(argc, params, 0, 0); @@ -2373,7 +2395,7 @@ static int cmd_ismusicstreamplaying(int argc, slib_par_t *params, var_t *retval) } // -// Checks if a music stream is valid (context and buffers initialized) +// Check if a music stream is valid (context and buffers initialized) // static int cmd_ismusicvalid(int argc, slib_par_t *params, var_t *retval) { int result; @@ -2441,7 +2463,7 @@ static int cmd_issoundplaying(int argc, slib_par_t *params, var_t *retval) { } // -// Checks if a sound is valid (data loaded and buffers initialized) +// Check if a sound is valid (data loaded and buffers initialized) // static int cmd_issoundvalid(int argc, slib_par_t *params, var_t *retval) { int result; @@ -2473,7 +2495,7 @@ static int cmd_istexturevalid(int argc, slib_par_t *params, var_t *retval) { } // -// Checks if wave data is valid (data loaded and parameters) +// Check if wave data is valid (data loaded and parameters) // static int cmd_iswavevalid(int argc, slib_par_t *params, var_t *retval) { int result; @@ -2595,7 +2617,7 @@ static int cmd_loadcodepoints(int argc, slib_par_t *params, var_t *retval) { } // -// Load directory filepaths +// Load directory filepaths, files and directories, no subdirs scan // static int cmd_loaddirectoryfiles(int argc, slib_par_t *params, var_t *retval) { auto dirPath = get_param_str(argc, params, 0, 0); @@ -2605,7 +2627,7 @@ static int cmd_loaddirectoryfiles(int argc, slib_par_t *params, var_t *retval) { } // -// Load directory filepaths with extension filtering and recursive directory scan. Use 'DIR' in the filter string to include directories in the result +// Load directory filepaths with extension filtering and subdir scan; some filters available: `*.*`,`FILES*`,`DIRS*` // static int cmd_loaddirectoryfilesex(int argc, slib_par_t *params, var_t *retval) { auto basePath = get_param_str(argc, params, 0, 0); @@ -2767,7 +2789,7 @@ static int cmd_loadimagefrommemory(int argc, slib_par_t *params, var_t *retval) } // -// Load image from screen buffer and (screenshot) +// Load image from screen buffer (screenshot) // static int cmd_loadimagefromscreen(int argc, slib_par_t *params, var_t *retval) { auto fnResult = LoadImageFromScreen(); @@ -2905,7 +2927,7 @@ static int cmd_loadsound(int argc, slib_par_t *params, var_t *retval) { } // -// Create a new sound that shares the same sample data as the source sound, does not own the sound data +// Load sound alias, new sound that shares the same sample data as the source sound, does not own the sound data // static int cmd_loadsoundalias(int argc, slib_par_t *params, var_t *retval) { int result; @@ -3049,6 +3071,26 @@ static int cmd_measuretext(int argc, slib_par_t *params, var_t *retval) { return 1; } +// +// Measure string size for an existing array of codepoints for Font +// +static int cmd_measuretextcodepoints(int argc, slib_par_t *params, var_t *retval) { + int result; + int font_id = get_font_id(argc, params, 0, retval); + if (font_id != -1) { + auto codepoints = (const int *)get_param_int_t(argc, params, 1, 0); + auto length = get_param_int(argc, params, 2, 0); + auto fontSize = get_param_num(argc, params, 3, 0); + auto spacing = get_param_num(argc, params, 4, 0); + auto fnResult = MeasureTextCodepoints(_fontMap.at(font_id), codepoints, length, fontSize, spacing); + v_setvec2(retval, fnResult); + result = 1; + } else { + result = 0; + } + return result; +} + // // Measure string size for Font // @@ -3094,7 +3136,7 @@ static int cmd_memrealloc(int argc, slib_par_t *params, var_t *retval) { // static int cmd_savefiledata(int argc, slib_par_t *params, var_t *retval) { auto fileName = get_param_str(argc, params, 0, 0); - auto data = (void *)get_param_int_t(argc, params, 1, 0); + auto data = (const void *)get_param_int_t(argc, params, 1, 0); auto dataSize = get_param_int(argc, params, 2, 0); auto fnResult = SaveFileData(fileName, data, dataSize); v_setint(retval, fnResult); @@ -3145,7 +3187,7 @@ static int cmd_textfindindex(int argc, slib_par_t *params, var_t *retval) { } // -// Insert text in a position (WARNING: memory must be freed!) +// Insert text in a defined byte position // static int cmd_textinsert(int argc, slib_par_t *params, var_t *retval) { auto text = get_param_str(argc, params, 0, 0); @@ -3157,7 +3199,19 @@ static int cmd_textinsert(int argc, slib_par_t *params, var_t *retval) { } // -// Check if two text string are equal +// Insert text in a defined byte position, memory must be MemFree() +// +static int cmd_textinsertalloc(int argc, slib_par_t *params, var_t *retval) { + auto text = get_param_str(argc, params, 0, 0); + auto insert = get_param_str(argc, params, 1, 0); + auto position = get_param_int(argc, params, 2, 0); + auto fnResult = (const char *)TextInsertAlloc(text, insert, position); + v_setstr(retval, fnResult); + return 1; +} + +// +// Check if two text strings are equal // static int cmd_textisequal(int argc, slib_par_t *params, var_t *retval) { auto text1 = get_param_str(argc, params, 0, 0); @@ -3188,7 +3242,7 @@ static int cmd_textremovespaces(int argc, slib_par_t *params, var_t *retval) { } // -// Replace text string (WARNING: memory must be freed!) +// Replace text string with new string // static int cmd_textreplace(int argc, slib_par_t *params, var_t *retval) { auto text = get_param_str(argc, params, 0, 0); @@ -3200,7 +3254,19 @@ static int cmd_textreplace(int argc, slib_par_t *params, var_t *retval) { } // -// Replace text between two specific strings (WARNING: memory must be freed!) +// Replace text string with new string, memory must be MemFree() +// +static int cmd_textreplacealloc(int argc, slib_par_t *params, var_t *retval) { + auto text = get_param_str(argc, params, 0, 0); + auto search = get_param_str(argc, params, 1, 0); + auto replacement = get_param_str(argc, params, 2, 0); + auto fnResult = (const char *)TextReplaceAlloc(text, search, replacement); + v_setstr(retval, fnResult); + return 1; +} + +// +// Replace text between two specific strings // static int cmd_textreplacebetween(int argc, slib_par_t *params, var_t *retval) { auto text = get_param_str(argc, params, 0, 0); @@ -3212,6 +3278,19 @@ static int cmd_textreplacebetween(int argc, slib_par_t *params, var_t *retval) { return 1; } +// +// Replace text between two specific strings, memory must be MemFree() +// +static int cmd_textreplacebetweenalloc(int argc, slib_par_t *params, var_t *retval) { + auto text = get_param_str(argc, params, 0, 0); + auto begin = get_param_str(argc, params, 1, 0); + auto end = get_param_str(argc, params, 2, 0); + auto replacement = get_param_str(argc, params, 3, 0); + auto fnResult = (const char *)TextReplaceBetweenAlloc(text, begin, end, replacement); + v_setstr(retval, fnResult); + return 1; +} + // // Get a piece of a text string // diff --git a/raylib/main.cpp b/raylib/main.cpp index 06598c9..ac63334 100644 --- a/raylib/main.cpp +++ b/raylib/main.cpp @@ -360,8 +360,7 @@ static FilePathList get_param_filepathlist(int argc, slib_par_t *params, int n) if (is_param_array(argc, params, n)) { var_p_t array = params[n].var_p; result.count = v_asize(array); - result.capacity = result.count; - result.paths = (char **)malloc(result.capacity * sizeof(char *)); + result.paths = (char **)malloc(result.count * sizeof(char *)); for (unsigned index = 0; index < result.count; index++) { result.paths[index] = (char *)malloc(MAX_FILEPATH_LENGTH * sizeof(char)); var_p_t elem = v_elem(array, index); @@ -750,7 +749,7 @@ static void v_setmodel(var_t *var, Model &model) { map_init_id(var, id, CLS_MODELMAP); v_setint(map_add_var(var, "meshCount", 0), model.meshCount); v_setint(map_add_var(var, "materialCount", 0), model.materialCount); - v_setint(map_add_var(var, "boneCount", 0), model.boneCount); + v_setint(map_add_var(var, "boneCount", 0), model.skeleton.boneCount); } static void v_setmusic(var_t *var, Music &music) { @@ -770,19 +769,19 @@ static void v_setmodel_animation(var_t *var, ModelAnimation *anims, int animsCou _modelAnimationMap[id] = anims[i]; map_init_id(v_anim, id, CLS_MODELANIMATIONMAP); - int frameCount = anims[i].frameCount; + int keyframeCount = anims[i].keyframeCount; int boneCount = anims[i].boneCount; - map_add_var(v_anim, "frameCount", frameCount); + map_add_var(v_anim, "keyframeCount", keyframeCount); map_add_var(v_anim, "boneCount", boneCount); var_t *v_framePoses = map_add_var(v_anim, "framePoses", 0); - v_tomatrix(v_framePoses, frameCount, boneCount); - for (int frame = 0; frame < frameCount; frame++) { + v_tomatrix(v_framePoses, keyframeCount, boneCount); + for (int frame = 0; frame < keyframeCount; frame++) { for (int bone = 0; bone < boneCount; bone++) { var_t *v_transform = v_elem(v_framePoses, ((boneCount * frame) + bone)); map_init(v_transform); - v_setvec3(map_add_var(v_transform, "translation", 0), anims[0].framePoses[frame][bone].translation); - v_setvec3(map_add_var(v_transform, "scale", 0), anims[0].framePoses[frame][bone].scale); + v_setvec3(map_add_var(v_transform, "translation", 0), anims[0].keyframePoses[frame][bone].translation); + v_setvec3(map_add_var(v_transform, "scale", 0), anims[0].keyframePoses[frame][bone].scale); } } } @@ -1979,7 +1978,7 @@ SBLIB_API int sblib_free(int cls_id, int id) { break; case CLS_MODELANIMATIONMAP: if (_modelAnimationMap.find(id) != _modelAnimationMap.end()) { - UnloadModelAnimation(_modelAnimationMap.at(id)); + UnloadModelAnimations(&_modelAnimationMap.at(id), 1); _modelAnimationMap.erase(id); } break; diff --git a/raylib/proc-def.h b/raylib/proc-def.h index e4456bf..fe4a164 100644 --- a/raylib/proc-def.h +++ b/raylib/proc-def.h @@ -19,7 +19,7 @@ {6, 6, "DRAWCAPSULEWIRES", cmd_drawcapsulewires}, {4, 4, "DRAWCIRCLE", cmd_drawcircle}, {5, 5, "DRAWCIRCLE3D", cmd_drawcircle3d}, - {5, 5, "DRAWCIRCLEGRADIENT", cmd_drawcirclegradient}, + {4, 4, "DRAWCIRCLEGRADIENT", cmd_drawcirclegradient}, {4, 4, "DRAWCIRCLELINES", cmd_drawcirclelines}, {3, 3, "DRAWCIRCLELINESV", cmd_drawcirclelinesv}, {6, 6, "DRAWCIRCLESECTOR", cmd_drawcirclesector}, @@ -48,8 +48,6 @@ {3, 3, "DRAWLINEV", cmd_drawlinev}, {4, 4, "DRAWMODEL", cmd_drawmodel}, {6, 6, "DRAWMODELEX", cmd_drawmodelex}, - {4, 4, "DRAWMODELPOINTS", cmd_drawmodelpoints}, - {6, 6, "DRAWMODELPOINTSEX", cmd_drawmodelpointsex}, {4, 4, "DRAWMODELWIRES", cmd_drawmodelwires}, {6, 6, "DRAWMODELWIRESEX", cmd_drawmodelwiresex}, {3, 3, "DRAWPIXEL", cmd_drawpixel}, @@ -101,6 +99,7 @@ {4, 4, "DRAWTRIANGLE", cmd_drawtriangle}, {4, 4, "DRAWTRIANGLE3D", cmd_drawtriangle3d}, {3, 3, "DRAWTRIANGLEFAN", cmd_drawtrianglefan}, + {6, 6, "DRAWTRIANGLEGRADIENT", cmd_drawtrianglegradient}, {4, 4, "DRAWTRIANGLELINES", cmd_drawtrianglelines}, {3, 3, "DRAWTRIANGLESTRIP", cmd_drawtrianglestrip}, {3, 3, "DRAWTRIANGLESTRIP3D", cmd_drawtrianglestrip3d}, @@ -142,14 +141,15 @@ {4, 4, "IMAGEDRAWPIXEL", cmd_imagedrawpixel}, {3, 3, "IMAGEDRAWPIXELV", cmd_imagedrawpixelv}, {6, 6, "IMAGEDRAWRECTANGLE", cmd_imagedrawrectangle}, - {4, 4, "IMAGEDRAWRECTANGLELINES", cmd_imagedrawrectanglelines}, + {6, 6, "IMAGEDRAWRECTANGLELINES", cmd_imagedrawrectanglelines}, + {4, 4, "IMAGEDRAWRECTANGLELINESEX", cmd_imagedrawrectanglelinesex}, {3, 3, "IMAGEDRAWRECTANGLEREC", cmd_imagedrawrectanglerec}, {4, 4, "IMAGEDRAWRECTANGLEV", cmd_imagedrawrectanglev}, {6, 6, "IMAGEDRAWTEXT", cmd_imagedrawtext}, {7, 7, "IMAGEDRAWTEXTEX", cmd_imagedrawtextex}, {5, 5, "IMAGEDRAWTRIANGLE", cmd_imagedrawtriangle}, - {7, 7, "IMAGEDRAWTRIANGLEEX", cmd_imagedrawtriangleex}, {4, 4, "IMAGEDRAWTRIANGLEFAN", cmd_imagedrawtrianglefan}, + {7, 7, "IMAGEDRAWTRIANGLEGRADIENT", cmd_imagedrawtrianglegradient}, {5, 5, "IMAGEDRAWTRIANGLELINES", cmd_imagedrawtrianglelines}, {4, 4, "IMAGEDRAWTRIANGLESTRIP", cmd_imagedrawtrianglestrip}, {1, 1, "IMAGEFLIPHORIZONTAL", cmd_imagefliphorizontal}, @@ -250,7 +250,6 @@ {1, 1, "UNLOADIMAGEPALETTE", cmd_unloadimagepalette}, {1, 1, "UNLOADMESH", cmd_unloadmesh}, {1, 1, "UNLOADMODEL", cmd_unloadmodel}, - {1, 1, "UNLOADMODELANIMATION", cmd_unloadmodelanimation}, {2, 2, "UNLOADMODELANIMATIONS", cmd_unloadmodelanimations}, {1, 1, "UNLOADMUSICSTREAM", cmd_unloadmusicstream}, {0, 0, "UNLOADRANDOMSEQUENCE", cmd_unloadrandomsequence}, @@ -265,7 +264,7 @@ {3, 3, "UPDATEAUDIOSTREAM", cmd_updateaudiostream}, {5, 5, "UPDATEMESHBUFFER", cmd_updatemeshbuffer}, {3, 3, "UPDATEMODELANIMATION", cmd_updatemodelanimation}, - {3, 3, "UPDATEMODELANIMATIONBONES", cmd_updatemodelanimationbones}, + {6, 6, "UPDATEMODELANIMATIONEX", cmd_updatemodelanimationex}, {1, 1, "UPDATEMUSICSTREAM", cmd_updatemusicstream}, {3, 3, "UPDATESOUND", cmd_updatesound}, {3, 3, "UPDATETEXTUREREC", cmd_updatetexturerec}, diff --git a/raylib/proc.h b/raylib/proc.h index f549c73..68e4e82 100644 --- a/raylib/proc.h +++ b/raylib/proc.h @@ -8,7 +8,7 @@ static int cmd_beginblendmode(int argc, slib_par_t *params, var_t *retval) { } // -// Setup canvas (framebuffer) to start drawing +// Begin canvas (framebuffer) drawing // static int cmd_begindrawing(int argc, slib_par_t *params, var_t *retval) { BeginDrawing(); @@ -70,7 +70,7 @@ static int cmd_begintexturemode(int argc, slib_par_t *params, var_t *retval) { } // -// Set background color (framebuffer clear color) +// Clear background (framebuffer) to color // static int cmd_clearbackground(int argc, slib_par_t *params, var_t *retval) { auto color = get_param_color(argc, params, 0); @@ -104,7 +104,7 @@ static int cmd_closewindow(int argc, slib_par_t *params, var_t *retval) { } // -// Disables cursor (lock cursor) +// Disable cursor (lock cursor) // static int cmd_disablecursor(int argc, slib_par_t *params, var_t *retval) { DisableCursor(); @@ -198,10 +198,10 @@ static int cmd_drawcapsule(int argc, slib_par_t *params, var_t *retval) { auto startPos = get_param_vec3(argc, params, 0); auto endPos = get_param_vec3(argc, params, 1); auto radius = get_param_num(argc, params, 2, 0); - auto slices = get_param_int(argc, params, 3, 0); - auto rings = get_param_int(argc, params, 4, 0); + auto rings = get_param_int(argc, params, 3, 0); + auto slices = get_param_int(argc, params, 4, 0); auto color = get_param_color(argc, params, 5); - DrawCapsule(startPos, endPos, radius, slices, rings, color); + DrawCapsule(startPos, endPos, radius, rings, slices, color); return 1; } @@ -212,10 +212,10 @@ static int cmd_drawcapsulewires(int argc, slib_par_t *params, var_t *retval) { auto startPos = get_param_vec3(argc, params, 0); auto endPos = get_param_vec3(argc, params, 1); auto radius = get_param_num(argc, params, 2, 0); - auto slices = get_param_int(argc, params, 3, 0); - auto rings = get_param_int(argc, params, 4, 0); + auto rings = get_param_int(argc, params, 3, 0); + auto slices = get_param_int(argc, params, 4, 0); auto color = get_param_color(argc, params, 5); - DrawCapsuleWires(startPos, endPos, radius, slices, rings, color); + DrawCapsuleWires(startPos, endPos, radius, rings, slices, color); return 1; } @@ -248,12 +248,11 @@ static int cmd_drawcircle3d(int argc, slib_par_t *params, var_t *retval) { // Draw a gradient-filled circle // static int cmd_drawcirclegradient(int argc, slib_par_t *params, var_t *retval) { - auto centerX = get_param_int(argc, params, 0, 0); - auto centerY = get_param_int(argc, params, 1, 0); - auto radius = get_param_num(argc, params, 2, 0); - auto inner = get_param_color(argc, params, 3); - auto outer = get_param_color(argc, params, 4); - DrawCircleGradient(centerX, centerY, radius, inner, outer); + auto center = get_param_vec2(argc, params, 0); + auto radius = get_param_num(argc, params, 1, 0); + auto inner = get_param_color(argc, params, 2); + auto outer = get_param_color(argc, params, 3); + DrawCircleGradient(center, radius, inner, outer); return 1; } @@ -417,9 +416,9 @@ static int cmd_drawcylinderwiresex(int argc, slib_par_t *params, var_t *retval) auto endPos = get_param_vec3(argc, params, 1); auto startRadius = get_param_num(argc, params, 2, 0); auto endRadius = get_param_num(argc, params, 3, 0); - auto sides = get_param_int(argc, params, 4, 0); + auto slices = get_param_int(argc, params, 4, 0); auto color = get_param_color(argc, params, 5); - DrawCylinderWiresEx(startPos, endPos, startRadius, endRadius, sides, color); + DrawCylinderWiresEx(startPos, endPos, startRadius, endRadius, slices, color); return 1; } @@ -614,44 +613,6 @@ static int cmd_drawmodelex(int argc, slib_par_t *params, var_t *retval) { return result; } -// -// Draw a model as points -// -static int cmd_drawmodelpoints(int argc, slib_par_t *params, var_t *retval) { - int result; - int model_id = get_model_id(argc, params, 0, retval); - if (model_id != -1) { - auto position = get_param_vec3(argc, params, 1); - auto scale = get_param_num(argc, params, 2, 0); - auto tint = get_param_color(argc, params, 3); - DrawModelPoints(_modelMap.at(model_id), position, scale, tint); - result = 1; - } else { - result = 0; - } - return result; -} - -// -// Draw a model as points with extended parameters -// -static int cmd_drawmodelpointsex(int argc, slib_par_t *params, var_t *retval) { - int result; - int model_id = get_model_id(argc, params, 0, retval); - if (model_id != -1) { - auto position = get_param_vec3(argc, params, 1); - auto rotationAxis = get_param_vec3(argc, params, 2); - auto rotationAngle = get_param_num(argc, params, 3, 0); - auto scale = get_param_vec3(argc, params, 4); - auto tint = get_param_color(argc, params, 5); - DrawModelPointsEx(_modelMap.at(model_id), position, rotationAxis, rotationAngle, scale, tint); - result = 1; - } else { - result = 0; - } - return result; -} - // // Draw a model wires (with texture if set) // @@ -733,7 +694,7 @@ static int cmd_drawpoint3d(int argc, slib_par_t *params, var_t *retval) { } // -// Draw a regular polygon (Vector version) +// Draw a polygon of n sides // static int cmd_drawpoly(int argc, slib_par_t *params, var_t *retval) { auto center = get_param_vec2(argc, params, 0); @@ -907,7 +868,7 @@ static int cmd_drawrectangleroundedlines(int argc, slib_par_t *params, var_t *re } // -// Draw rectangle with rounded edges outline +// Draw rectangle lines with rounded edges outline // static int cmd_drawrectangleroundedlinesex(int argc, slib_par_t *params, var_t *retval) { auto rec = get_param_rect(argc, params, 0); @@ -1157,7 +1118,7 @@ static int cmd_drawtextcodepoint(int argc, slib_par_t *params, var_t *retval) { } // -// Draw multiple character (codepoint) +// Draw multiple characters (codepoint) // static int cmd_drawtextcodepoints(int argc, slib_par_t *params, var_t *retval) { int result; @@ -1257,7 +1218,7 @@ static int cmd_drawtextureex(int argc, slib_par_t *params, var_t *retval) { } // -// Draws a texture (or part of it) that stretches or shrinks nicely +// Draw a texture (or part of it) that stretches or shrinks nicely // static int cmd_drawtexturenpatch(int argc, slib_par_t *params, var_t *retval) { int result; @@ -1366,6 +1327,20 @@ static int cmd_drawtrianglefan(int argc, slib_par_t *params, var_t *retval) { return 1; } +// +// Draw triangle with interpolated colors (vertex in counter-clockwise order!) +// +static int cmd_drawtrianglegradient(int argc, slib_par_t *params, var_t *retval) { + auto v1 = get_param_vec2(argc, params, 0); + auto v2 = get_param_vec2(argc, params, 1); + auto v3 = get_param_vec2(argc, params, 2); + auto c1 = get_param_color(argc, params, 3); + auto c2 = get_param_color(argc, params, 4); + auto c3 = get_param_color(argc, params, 5); + DrawTriangleGradient(v1, v2, v3, c1, c2, c3); + return 1; +} + // // Draw triangle outline (vertex in counter-clockwise order!) // @@ -1401,7 +1376,7 @@ static int cmd_drawtrianglestrip3d(int argc, slib_par_t *params, var_t *retval) } // -// Enables cursor (unlock cursor) +// Enable cursor (unlock cursor) // static int cmd_enablecursor(int argc, slib_par_t *params, var_t *retval) { EnableCursor(); @@ -1425,7 +1400,7 @@ static int cmd_endblendmode(int argc, slib_par_t *params, var_t *retval) { } // -// End canvas drawing and swap buffers (double buffering) +// End canvas (framebuffer) drawing and swap buffers (double buffering) // static int cmd_enddrawing(int argc, slib_par_t *params, var_t *retval) { EndDrawing(); @@ -1433,7 +1408,7 @@ static int cmd_enddrawing(int argc, slib_par_t *params, var_t *retval) { } // -// Ends 2D mode with custom camera +// End 2D mode with custom camera // static int cmd_endmode2d(int argc, slib_par_t *params, var_t *retval) { EndMode2D(); @@ -1441,7 +1416,7 @@ static int cmd_endmode2d(int argc, slib_par_t *params, var_t *retval) { } // -// Ends 3D mode and returns to default 2D orthographic mode +// End 3D mode and returns to default 2D orthographic mode // static int cmd_endmode3d(int argc, slib_par_t *params, var_t *retval) { EndMode3D(); @@ -1465,7 +1440,7 @@ static int cmd_endshadermode(int argc, slib_par_t *params, var_t *retval) { } // -// Ends drawing to render texture +// End drawing to render texture // static int cmd_endtexturemode(int argc, slib_par_t *params, var_t *retval) { EndTextureMode(); @@ -1499,7 +1474,7 @@ static int cmd_gentexturemipmaps(int argc, slib_par_t *params, var_t *retval) { } // -// Hides cursor +// Hide cursor // static int cmd_hidecursor(int argc, slib_par_t *params, var_t *retval) { HideCursor(); @@ -1625,7 +1600,7 @@ static int cmd_imagecolorcontrast(int argc, slib_par_t *params, var_t *retval) { int result; int image_id = get_image_id(argc, params, 0, retval); if (image_id != -1) { - auto contrast = get_param_num(argc, params, 1, 0); + auto contrast = get_param_int(argc, params, 1, 0); ImageColorContrast(&_imageMap.at(image_id), contrast); result = 1; } else { @@ -1941,13 +1916,33 @@ static int cmd_imagedrawrectangle(int argc, slib_par_t *params, var_t *retval) { // Draw rectangle lines within an image // static int cmd_imagedrawrectanglelines(int argc, slib_par_t *params, var_t *retval) { + int result; + int dst_id = get_image_id(argc, params, 0, retval); + if (dst_id != -1) { + auto posX = get_param_int(argc, params, 1, 0); + auto posY = get_param_int(argc, params, 2, 0); + auto width = get_param_int(argc, params, 3, 0); + auto height = get_param_int(argc, params, 4, 0); + auto color = get_param_color(argc, params, 5); + ImageDrawRectangleLines(&_imageMap.at(dst_id), posX, posY, width, height, color); + result = 1; + } else { + result = 0; + } + return result; +} + +// +// Draw rectangle lines within an image with extended parameters +// +static int cmd_imagedrawrectanglelinesex(int argc, slib_par_t *params, var_t *retval) { int result; int dst_id = get_image_id(argc, params, 0, retval); if (dst_id != -1) { auto rec = get_param_rect(argc, params, 1); auto thick = get_param_int(argc, params, 2, 0); auto color = get_param_color(argc, params, 3); - ImageDrawRectangleLines(&_imageMap.at(dst_id), rec, thick, color); + ImageDrawRectangleLinesEx(&_imageMap.at(dst_id), rec, thick, color); result = 1; } else { result = 0; @@ -2051,19 +2046,16 @@ static int cmd_imagedrawtriangle(int argc, slib_par_t *params, var_t *retval) { } // -// Draw triangle with interpolated colors within an image +// Draw a triangle fan defined by points within an image (first vertex is the center) // -static int cmd_imagedrawtriangleex(int argc, slib_par_t *params, var_t *retval) { +static int cmd_imagedrawtrianglefan(int argc, slib_par_t *params, var_t *retval) { int result; int dst_id = get_image_id(argc, params, 0, retval); if (dst_id != -1) { - auto v1 = get_param_vec2(argc, params, 1); - auto v2 = get_param_vec2(argc, params, 2); - auto v3 = get_param_vec2(argc, params, 3); - auto c1 = get_param_color(argc, params, 4); - auto c2 = get_param_color(argc, params, 5); - auto c3 = get_param_color(argc, params, 6); - ImageDrawTriangleEx(&_imageMap.at(dst_id), v1, v2, v3, c1, c2, c3); + auto points = (Vector2 *)get_param_vec2_array(argc, params, 1); + auto pointCount = get_param_int(argc, params, 2, 0); + auto color = get_param_color(argc, params, 3); + ImageDrawTriangleFan(&_imageMap.at(dst_id), points, pointCount, color); result = 1; } else { result = 0; @@ -2072,16 +2064,19 @@ static int cmd_imagedrawtriangleex(int argc, slib_par_t *params, var_t *retval) } // -// Draw a triangle fan defined by points within an image (first vertex is the center) +// Draw triangle with interpolated colors within an image // -static int cmd_imagedrawtrianglefan(int argc, slib_par_t *params, var_t *retval) { +static int cmd_imagedrawtrianglegradient(int argc, slib_par_t *params, var_t *retval) { int result; int dst_id = get_image_id(argc, params, 0, retval); if (dst_id != -1) { - auto points = (Vector2 *)get_param_vec2_array(argc, params, 1); - auto pointCount = get_param_int(argc, params, 2, 0); - auto color = get_param_color(argc, params, 3); - ImageDrawTriangleFan(&_imageMap.at(dst_id), points, pointCount, color); + auto v1 = get_param_vec2(argc, params, 1); + auto v2 = get_param_vec2(argc, params, 2); + auto v3 = get_param_vec2(argc, params, 3); + auto c1 = get_param_color(argc, params, 4); + auto c2 = get_param_color(argc, params, 5); + auto c3 = get_param_color(argc, params, 6); + ImageDrawTriangleGradient(&_imageMap.at(dst_id), v1, v2, v3, c1, c2, c3); result = 1; } else { result = 0; @@ -2542,7 +2537,7 @@ static int cmd_setaudiostreambuffersizedefault(int argc, slib_par_t *params, var } // -// Set pan for audio stream (0.5 is centered) +// Set pan for audio stream (-1.0 left, 0.0 center, 1.0 right) // static int cmd_setaudiostreampan(int argc, slib_par_t *params, var_t *retval) { int result; @@ -2623,7 +2618,7 @@ static int cmd_setclipboardtext(int argc, slib_par_t *params, var_t *retval) { } // -// Setup init configuration flags (view FLAGS) +// Set up init configuration flags (view FLAGS) // static int cmd_setconfigflags(int argc, slib_par_t *params, var_t *retval) { auto flags = get_param_int(argc, params, 0, 0); @@ -2721,7 +2716,7 @@ static int cmd_setmousescale(int argc, slib_par_t *params, var_t *retval) { } // -// Set pan for a music (-1.0 left, 0.0 center, 1.0 right) +// Set pan for music (-1.0 left, 0.0 center, 1.0 right) // static int cmd_setmusicpan(int argc, slib_par_t *params, var_t *retval) { int result; @@ -2737,7 +2732,7 @@ static int cmd_setmusicpan(int argc, slib_par_t *params, var_t *retval) { } // -// Set pitch for a music (1.0 is base level) +// Set pitch for music (1.0 is base level) // static int cmd_setmusicpitch(int argc, slib_par_t *params, var_t *retval) { int result; @@ -3074,7 +3069,7 @@ static int cmd_setwindowtitle(int argc, slib_par_t *params, var_t *retval) { } // -// Shows cursor +// Show cursor // static int cmd_showcursor(int argc, slib_par_t *params, var_t *retval) { ShowCursor(); @@ -3336,22 +3331,6 @@ static int cmd_unloadmodel(int argc, slib_par_t *params, var_t *retval) { return result; } -// -// Unload animation data -// -static int cmd_unloadmodelanimation(int argc, slib_par_t *params, var_t *retval) { - int result; - int anim_id = get_model_animation_id(argc, params, 0, retval); - if (anim_id != -1) { - UnloadModelAnimation(_modelAnimationMap.at(anim_id)); - _modelAnimationMap.erase(anim_id); - result = 1; - } else { - result = 0; - } - return result; -} - // // Unload animation array data // @@ -3429,7 +3408,7 @@ static int cmd_unloadsound(int argc, slib_par_t *params, var_t *retval) { } // -// Unload a sound alias (does not deallocate sample data) +// Unload sound alias (does not deallocate sample data) // static int cmd_unloadsoundalias(int argc, slib_par_t *params, var_t *retval) { int result; @@ -3531,14 +3510,14 @@ static int cmd_updatemeshbuffer(int argc, slib_par_t *params, var_t *retval) { } // -// Update model animation pose (CPU) +// Update model animation pose (vertex buffers and bone matrices) // static int cmd_updatemodelanimation(int argc, slib_par_t *params, var_t *retval) { int result; int model_id = get_model_id(argc, params, 0, retval); int anim_id = get_model_animation_id(argc, params, 1, retval); if (model_id != -1 && anim_id != -1) { - auto frame = get_param_int(argc, params, 2, 0); + auto frame = get_param_num(argc, params, 2, 0); UpdateModelAnimation(_modelMap.at(model_id), _modelAnimationMap.at(anim_id), frame); result = 1; } else { @@ -3548,15 +3527,18 @@ static int cmd_updatemodelanimation(int argc, slib_par_t *params, var_t *retval) } // -// Update model animation mesh bone matrices (GPU skinning) +// Update model animation pose, blending two animations // -static int cmd_updatemodelanimationbones(int argc, slib_par_t *params, var_t *retval) { +static int cmd_updatemodelanimationex(int argc, slib_par_t *params, var_t *retval) { int result; int model_id = get_model_id(argc, params, 0, retval); - int anim_id = get_model_animation_id(argc, params, 1, retval); - if (model_id != -1 && anim_id != -1) { - auto frame = get_param_int(argc, params, 2, 0); - UpdateModelAnimationBones(_modelMap.at(model_id), _modelAnimationMap.at(anim_id), frame); + int anima_id = get_model_animation_id(argc, params, 1, retval); + int animb_id = get_model_animation_id(argc, params, 3, retval); + if (model_id != -1 && anima_id != -1 && animb_id != -1) { + auto frameA = get_param_num(argc, params, 2, 0); + auto frameB = get_param_num(argc, params, 4, 0); + auto blend = get_param_num(argc, params, 5, 0); + UpdateModelAnimationEx(_modelMap.at(model_id), _modelAnimationMap.at(anima_id), frameA, _modelAnimationMap.at(animb_id), frameB, blend); result = 1; } else { result = 0; @@ -3565,7 +3547,7 @@ static int cmd_updatemodelanimationbones(int argc, slib_par_t *params, var_t *re } // -// Updates buffers for music streaming +// Update buffers for music streaming // static int cmd_updatemusicstream(int argc, slib_par_t *params, var_t *retval) { int result; @@ -3587,8 +3569,8 @@ static int cmd_updatesound(int argc, slib_par_t *params, var_t *retval) { int sound_id = get_sound_id(argc, params, 0, retval); if (sound_id != -1) { auto data = (const void *)get_param_int_t(argc, params, 1, 0); - auto sampleCount = get_param_int(argc, params, 2, 0); - UpdateSound(_soundMap.at(sound_id), data, sampleCount); + auto frameCount = get_param_int(argc, params, 2, 0); + UpdateSound(_soundMap.at(sound_id), data, frameCount); result = 1; } else { result = 0; diff --git a/raylib/raygui b/raylib/raygui index 9a95871..6d2b28f 160000 --- a/raylib/raygui +++ b/raylib/raygui @@ -1 +1 @@ -Subproject commit 9a95871701a5fc63bea35eab73fef6414e048b73 +Subproject commit 6d2b28ff748158be0d63d07988d5d0672905dedf diff --git a/raylib/raylib b/raylib/raylib index c610d22..95bfa19 160000 --- a/raylib/raylib +++ b/raylib/raylib @@ -1 +1 @@ -Subproject commit c610d228a244f930ad53492604640f39584c66da +Subproject commit 95bfa196fdfb737356b8a09ab2944e765a71280a diff --git a/websocket/main.cpp b/websocket/main.cpp index a794e57..2f27efb 100644 --- a/websocket/main.cpp +++ b/websocket/main.cpp @@ -193,9 +193,9 @@ static void client_handler(mg_connection *conn, int event, void *eventData) { static void set_session(var_p_t var, Session *session, mg_connection *conn) { session->setConnection(conn); map_init_id(var, conn->id); - v_setstr(map_add_var(var, "local_ip", 0), (const char *)conn->loc.ip); + v_setstr(map_add_var(var, "local_ip", 0), (const char *)conn->loc.addr.ip); v_setint(map_add_var(var, "local_port", 0), conn->loc.port); - v_setstr(map_add_var(var, "remote_ip", 0), (const char *)conn->rem.ip); + v_setstr(map_add_var(var, "remote_ip", 0), (const char *)conn->rem.addr.ip); v_setint(map_add_var(var, "remote_port", 0), conn->rem.port); } diff --git a/websocket/mongoose b/websocket/mongoose index 55bc610..eee8c70 160000 --- a/websocket/mongoose +++ b/websocket/mongoose @@ -1 +1 @@ -Subproject commit 55bc6105e148633ddc65bddbdb307f1477c0fc01 +Subproject commit eee8c7077c031f22469ea3adfcc69fc6d86c479a From 66d874c9044ac3750c9d6206eb9cf46fe9f60a60 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Wed, 13 May 2026 17:21:45 +0930 Subject: [PATCH 104/131] LLAMA: implemented mem_info command --- llama/llama-sb.cpp | 65 ++++++++++++++++++++++++++++++++++++++++-- llama/llama-sb.h | 24 ++++++++++++++++ llama/main.cpp | 30 +++++++++++++++++++ llama/samples/nitro.md | 2 ++ 4 files changed, 118 insertions(+), 3 deletions(-) diff --git a/llama/llama-sb.cpp b/llama/llama-sb.cpp index 8bb0291..4d4cf7c 100644 --- a/llama/llama-sb.cpp +++ b/llama/llama-sb.cpp @@ -8,12 +8,26 @@ #include #include #include +#include "ggml-cuda.h" #include "llama.h" #include "llama-sb.h" constexpr int MAX_REPEAT = 5; +static bool read_vram(size_t &used, size_t &total) { + size_t free = 0; + total = 0; +#ifdef GGML_USE_CUDA + ggml_backend_cuda_get_device_memory(0, &free, &total); + if (total > 0) { + used = total - free; + return true; + } +#endif + return false; +} + LlamaIter::LlamaIter() : _llama(nullptr), _repetition_count(0), @@ -45,6 +59,7 @@ Llama::Llama() : _top_k(0), _max_tokens(0), _log_level(GGML_LOG_LEVEL_CONT), + _n_gpu_layers(0), _n_past(0), _is_gemma4(false), _seed(LLAMA_DEFAULT_SEED) { @@ -78,6 +93,7 @@ Llama::Llama(Llama &&other) noexcept , _top_k(other._top_k) , _max_tokens(other._max_tokens) , _log_level(other._log_level) + , _n_gpu_layers(other._n_gpu_layers) , _n_past(other._n_past) , _is_gemma4(other._is_gemma4) , _seed(other._seed) { @@ -128,6 +144,7 @@ bool Llama::construct(string model_path, int n_ctx, int n_batch, int n_gpu_layer } _log_level = log_level; + _n_gpu_layers = n_gpu_layers; _model = llama_model_load_from_file(model_path.c_str(), mparams); if (!_model) { _last_error = "Failed to load model"; @@ -141,8 +158,8 @@ bool Llama::construct(string model_path, int n_ctx, int n_batch, int n_gpu_layer cparams.flash_attn_type = LLAMA_FLASH_ATTN_TYPE_ENABLED; // or Q4_0 for more aggressive saving - cparams.type_k = GGML_TYPE_Q8_0; - cparams.type_v = GGML_TYPE_Q8_0; + cparams.type_k = GGML_TYPE_Q4_0; + cparams.type_v = GGML_TYPE_Q4_0; // keep KV cache on GPU cparams.offload_kqv = true; @@ -331,7 +348,8 @@ bool Llama::add_message(LlamaIter &iter, const string &role, const string &conte llama_batch batch = llama_batch_get_one(prompt_tokens.data() + i, batch_size); int result = llama_decode(_ctx, batch); if (result != 0) { - _last_error = std::format("Failed to decode batch. position:{} error:{}", i, result); + _last_error = std::format("Failed to decode batch. position:{} error:{} [size:{}, past:{}]", + i, result, prompt_tokens.size(), _n_past); return false; } } @@ -506,3 +524,44 @@ string Llama::all(LlamaIter &iter) { return out; } + +LlamaMemoryInfo Llama::memory_info() { + LlamaMemoryInfo info = {}; + + // KV cache usage + llama_memory_t mem = llama_get_memory(_ctx); + llama_pos pos_max = llama_memory_seq_pos_max(mem, 0); + int n_ctx = llama_n_ctx(_ctx); + info.kv_total = n_ctx; + info.kv_used = (pos_max < 0) ? 0 : (int)pos_max + 1; + info.kv_percent = 100.0f * info.kv_used / info.kv_total; + + // Model layers + info.n_layers_total = llama_model_n_layer(_model); + info.n_layers_gpu = _n_gpu_layers; + info.n_layers_cpu = info.n_layers_total - info.n_layers_gpu; + + // ram + if (read_vram(info.vram_used, info.vram_total)) { + info.vram_percent = 100.0f * info.vram_used / info.vram_total; + } + + // Advice + ostringstream advice; + if (info.n_layers_cpu > 0) { + advice << "CPU offload active (" << info.n_layers_cpu + << " layers on CPU) - increase n_gpu_layers if VRAM allows. "; + } + if (info.vram_percent > 90.0f) { + advice << "VRAM >90% - reduce n_ctx or use Q4_0 KV cache. "; + } else if (info.vram_percent < 60.0f && info.n_layers_cpu > 0) { + advice << "VRAM headroom available - try adding more GPU layers. "; + } + if (info.kv_percent > 80.0f) { + advice << "Context >80% full - consider calling clear_history(). "; + } + info.advice = advice.str(); + + return info; +} + diff --git a/llama/llama-sb.h b/llama/llama-sb.h index 30714a1..79dc2e3 100644 --- a/llama/llama-sb.h +++ b/llama/llama-sb.h @@ -14,6 +14,26 @@ using namespace std; +struct LlamaMemoryInfo { + // KV cache + int kv_used; // slots currently used + int kv_total; // total slots (== n_ctx) + float kv_percent; // kv_used / kv_total + + // GPU VRAM (via ggml backend) + size_t vram_used; // bytes + size_t vram_total; // bytes + float vram_percent; + + // Model layers + int n_layers_total; // total model layers + int n_layers_gpu; // layers offloaded to GPU + int n_layers_cpu; // layers on CPU + + // Advice + string advice; +}; + struct Llama; struct LlamaIter { @@ -75,6 +95,9 @@ struct Llama { void set_log_level(int level) { _log_level = level; } void reset(); + // memory info + LlamaMemoryInfo memory_info(); + private: bool ends_with_sentence_boundary(const string &out); bool configure_sampler(); @@ -102,6 +125,7 @@ struct Llama { int _top_k; int _max_tokens; int _log_level; + int _n_gpu_layers; int _n_past; bool _is_gemma4; unsigned int _seed; diff --git a/llama/main.cpp b/llama/main.cpp index 9eede4a..aa235d9 100644 --- a/llama/main.cpp +++ b/llama/main.cpp @@ -431,6 +431,35 @@ static int cmd_llama_add_message(var_s *self, int argc, slib_par_t *arg, var_s * return result; } +// +// print llama.mem_info() +// +static int cmd_llama_mem_info(var_s *self, int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + if (argc != 0) { + error(retval, "llama.mem_info", 0, 0); + } else { + int id = get_llama_class_id(self, retval); + if (id != -1) { + Llama &llama = g_llama.at(id); + auto mem_info = llama.memory_info(); + map_init(retval); + v_setint(map_add_var(retval, "kv_used", 0), mem_info.kv_used); + v_setint(map_add_var(retval, "kv_total", 0), mem_info.kv_total); + v_setreal(map_add_var(retval, "kv_percent", 0), mem_info.kv_percent); + v_setint(map_add_var(retval, "vram_used", 0), mem_info.vram_used); + v_setint(map_add_var(retval, "vram_total", 0), mem_info.vram_total); + v_setreal(map_add_var(retval, "vram_percent", 0), mem_info.vram_percent); + v_setint(map_add_var(retval, "n_layers_cpu", 0), mem_info.n_layers_cpu); + v_setint(map_add_var(retval, "n_layers_gpu", 0), mem_info.n_layers_gpu); + v_setint(map_add_var(retval, "n_layers_total", 0), mem_info.n_layers_total); + v_setstr(map_add_var(retval, "advice", 0), mem_info.advice.c_str()); + result = 1; + } + } + return result; +} + static int cmd_create_llama(int argc, slib_par_t *params, var_t *retval) { int result; auto model = expand_path(get_param_str(argc, params, 0, "")); @@ -456,6 +485,7 @@ static int cmd_create_llama(int argc, slib_par_t *params, var_t *retval) { v_create_callback(retval, "set_top_p", cmd_llama_set_top_p); v_create_callback(retval, "set_grammar", cmd_llama_set_grammar); v_create_callback(retval, "set_seed", cmd_llama_set_seed); + v_create_callback(retval, "mem_info", cmd_llama_mem_info); result = 1; } else { error(retval, llama.last_error()); diff --git a/llama/samples/nitro.md b/llama/samples/nitro.md index 8fa570a..cae4896 100644 --- a/llama/samples/nitro.md +++ b/llama/samples/nitro.md @@ -53,6 +53,8 @@ Available commands: - TOOL:LIST `[directory_path. items enclosed in square brackets (`[...]`) represent directories within the file listing output]` - TOOL:READ `[file_path]` - TOOL:WRITE `[file_path]` +- TOOL:EXISTS `[file_path]` +- TOOL:PERMISSION `[Request user permission before overwriting a file]` - TOOL:DATE `[Returns the current date as string with format β€œDD/MM/YYYY”]` - TOOL:TIME `[Returns the time in β€œHH:MM:SS” format]` - TOOL:RND [Returns a random number betweem 0 and 1]` From 5bff373bddd14a1a90e931414af8d11a66fc3933 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Wed, 13 May 2026 20:01:15 +0930 Subject: [PATCH 105/131] LLAMA: implemented mem_info command --- llama/llama-sb.cpp | 10 +++++++++- llama/llama.cpp | 2 +- llama/samples/nitro_cli.bas | 36 +++++++++++++++++++++++++----------- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/llama/llama-sb.cpp b/llama/llama-sb.cpp index 4d4cf7c..4fde33d 100644 --- a/llama/llama-sb.cpp +++ b/llama/llama-sb.cpp @@ -537,8 +537,9 @@ LlamaMemoryInfo Llama::memory_info() { info.kv_percent = 100.0f * info.kv_used / info.kv_total; // Model layers + auto n_gpu_layers = std::max(0, _n_gpu_layers); info.n_layers_total = llama_model_n_layer(_model); - info.n_layers_gpu = _n_gpu_layers; + info.n_layers_gpu = std::min(info.n_layers_total, n_gpu_layers); info.n_layers_cpu = info.n_layers_total - info.n_layers_gpu; // ram @@ -548,6 +549,13 @@ LlamaMemoryInfo Llama::memory_info() { // Advice ostringstream advice; + + if (n_gpu_layers < info.n_layers_total) { + advice << "Only " << n_gpu_layers << "/" << info.n_layers_total + << " layers on GPU - increase n_gpu_layers if VRAM allows. "; + } else { + advice << "All " << info.n_layers_total << " layers on GPU. "; + } if (info.n_layers_cpu > 0) { advice << "CPU offload active (" << info.n_layers_cpu << " layers on CPU) - increase n_gpu_layers if VRAM allows. "; diff --git a/llama/llama.cpp b/llama/llama.cpp index 58e68df..5d44db6 160000 --- a/llama/llama.cpp +++ b/llama/llama.cpp @@ -1 +1 @@ -Subproject commit 58e68df0f91dd16ff56423ee5ef44062ed73bdfc +Subproject commit 5d44db60089b0381cdbf7c45ce9ded43fc0c7f4c diff --git a/llama/samples/nitro_cli.bas b/llama/samples/nitro_cli.bas index 7a94294..690ebf5 100644 --- a/llama/samples/nitro_cli.bas +++ b/llama/samples/nitro_cli.bas @@ -20,7 +20,7 @@ const WHITE = chr(27) + "[37m" const BOLD_CYAN = chr(27) + "[1;36m" ' llama configuration (quen settings) -const n_ctx = 32768 +const n_ctx = 65536 const n_batch = 512 const n_max_tokens = 4096 const n_temperature = 0.6 @@ -29,8 +29,12 @@ const n_top_p = 0.95 const n_min_p = 0 const n_penalty_repeat = 1.0 const n_penalty_last_n = 256 +const n_gpu_layers = 32 -sandbox_home = cwd +sandbox_home = iff(len(command) > 0, trim(command), cwd) +if (left(sandbox_home) == "~") then + sandbox_home = home + mid(sandbox_home, 1) +endif ' ' Displays the welcome message @@ -50,8 +54,8 @@ end sub ' handles the TOOL:LIST command ' func tool_list_files(arg) - if (arg == "./") then - arg = sandbox_home + arg + if (left(arg, 2) == "./") then + arg = sandbox_home + mid(arg, 2) else if (len(arg) == 0 or arg == ".") then arg = sandbox_home endif @@ -151,11 +155,11 @@ func process_tool(cmd) endif endif - ' print RED - ' print "["+op+"]" - ' print "["+arg1+"]" - ' print "["+arg2+"]" - ' print RESET + ' print RED + ' print "["+op+"]" + ' print "["+arg1+"]" + ' print "["+arg2+"]" + ' print RESET select case op case "TOOL:DATE" @@ -219,7 +223,7 @@ end ' creates the llama instance ' func create_llama() - local llama = llm.llama(model, n_ctx, n_batch, 50) + local llama = llm.llama(model, n_ctx, n_batch, n_gpu_layers) llama.add_stop("<|turn|>") llama.set_max_tokens(n_max_tokens) llama.set_temperature(n_temperature) @@ -279,7 +283,16 @@ sub main() print print WHITE; print "--- Tokens/sec: " + round(iter.tokens_sec(), 2) + " ---\n" - iter = llama.add_message("user", process_input()) + local next_iter = false + repeat + local user_input = process_input() + if (user_input == "/meminfo") then + print llama.mem_info() + else + iter = llama.add_message("user", user_input) + next_iter = true + endif + until next_iter print BLUE; endif wend @@ -287,3 +300,4 @@ end welcome_message() main() + From 46cafef2f301f4a781a87dc0e8e5b801b2f190b9 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Wed, 13 May 2026 21:59:41 +0930 Subject: [PATCH 106/131] LLAMA: updated nitro agent - wip --- llama/samples/nitro_cli.bas | 51 ++++++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/llama/samples/nitro_cli.bas b/llama/samples/nitro_cli.bas index 690ebf5..3450b42 100644 --- a/llama/samples/nitro_cli.bas +++ b/llama/samples/nitro_cli.bas @@ -31,10 +31,18 @@ const n_penalty_repeat = 1.0 const n_penalty_last_n = 256 const n_gpu_layers = 32 -sandbox_home = iff(len(command) > 0, trim(command), cwd) -if (left(sandbox_home) == "~") then - sandbox_home = home + mid(sandbox_home, 1) -endif +' +' joins the left and right sides with forward slash +' +func join_path(s1, s2) + if (right(s1, 1) == "/") then + s1 = left(s1, len(s1) - 1) + endif + if (left(s2, 1) == "/") then + s2 = mid(s2, 2) + endif + return s1 + "/" + s2 +end ' ' Displays the welcome message @@ -82,9 +90,9 @@ end ' func tool_read_file(arg) try - tload sandbox_home + arg, result, 1 + tload join_path(sandbox_home, arg), result, 1 catch - result = "ERROR: File not found or unreadable." + result = "ERROR: File not found or unreadable: " + arg end try return result end @@ -120,7 +128,7 @@ end ' func tool_write_file(arg, s) try - tsave sandbox_home + arg, s + tsave join_path(sandbox_home, arg), s result = "OK: Data written successfully to " + arg catch e result = "ERROR: " + e @@ -133,8 +141,13 @@ end ' func tool_permission() local k - input "Agree?"; k - return iff(trim(k) == "YES", "YES", "NO") + input "Agree:? ", k + select case lower(k) + case "y", "yes", "sure", "okay", "k" + return "YES" + case else + return "NO" + end select end ' @@ -155,11 +168,11 @@ func process_tool(cmd) endif endif - ' print RED - ' print "["+op+"]" - ' print "["+arg1+"]" - ' print "["+arg2+"]" - ' print RESET + print RED + print "["+op+"]" + print "["+arg1+"]" + print "["+arg2+"]" + print RESET select case op case "TOOL:DATE" @@ -182,7 +195,7 @@ func process_tool(cmd) result = "ERROR: unknown command " + op end select - 'print RED + "TOOL RESULT:" + result + RESET + print RED + "TOOL RESULT:" + result + RESET return result end @@ -298,6 +311,14 @@ sub main() wend end +sandbox_home = iff(len(command) > 0, trim(command), cwd) +if (left(sandbox_home) == "~") then + sandbox_home = join_path(home, mid(sandbox_home, 1)) +else if (left(sandbox_home, 2) == "./") then + sandbox_home = join_path(cwd, mid(sandbox_home, 2)) +endif + welcome_message() main() + From 6c278bdac4f9840911eaf749d186c93b185e4c89 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Sun, 17 May 2026 17:07:27 +0930 Subject: [PATCH 107/131] LLAMA: RAG experiment to increase domain knowledge of a particular lib --- llama/CMakeLists.txt | 22 ++ llama/RAG.md | 325 ++++++++++++++++++++++++ llama/chunk_headers.cpp | 311 +++++++++++++++++++++++ llama/llama-sb.cpp | 479 ++++++++++++++++++++++-------------- llama/llama-sb.h | 12 +- llama/llama.cpp | 2 +- llama/main.cpp | 2 +- llama/rag.hpp | 328 ++++++++++++++++++++++++ llama/rag_index.cpp | 200 +++++++++++++++ llama/samples/adventure.bas | 2 +- llama/samples/nitro.md | 1 + llama/samples/nitro_cli.bas | 19 +- llama/test_main.cpp | 2 +- 13 files changed, 1508 insertions(+), 197 deletions(-) create mode 100644 llama/RAG.md create mode 100644 llama/chunk_headers.cpp create mode 100644 llama/rag.hpp create mode 100644 llama/rag_index.cpp diff --git a/llama/CMakeLists.txt b/llama/CMakeLists.txt index 8eee40e..629a269 100644 --- a/llama/CMakeLists.txt +++ b/llama/CMakeLists.txt @@ -171,6 +171,28 @@ set_target_properties(llm_test PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin ) +# ----------------------------- +# RAG indexer +# ----------------------------- +add_executable(rag_index + rag_index.cpp +) + +target_include_directories(rag_index PRIVATE + ${LLAMA_DIR}/include + ${LLAMA_DIR}/ggml/include +) + +target_link_libraries(rag_index PRIVATE + llm + llama + ggml +) + +set_target_properties(rag_index PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin +) + # ------------------------------------------------------------------ # Android native library # ------------------------------------------------------------------ diff --git a/llama/RAG.md b/llama/RAG.md new file mode 100644 index 0000000..ee5e8e7 --- /dev/null +++ b/llama/RAG.md @@ -0,0 +1,325 @@ +# notcurses RAG β€” C++ Library Expert via llama.cpp + +A self-contained RAG (Retrieval-Augmented Generation) pipeline in C++17 +that turns a GGUF inference model into a focused expert on any C/C++ library. +Demonstrated here with [notcurses](https://github.com/dankamongmen/notcurses) +but works with any header-based library. + +No fixed limits on chunk count, chunk length, or embedding dimension. +No Python, no vector database daemon, no external dependencies beyond llama.cpp. + +--- + +## How it works + +``` +INDEXING (one-time offline) +──────────────────────────────────────────────────────────────── +notcurses headers + β”‚ + β–Ό +chunk_headers ← semantic chunker, outputs chunks.jsonl + β”‚ + β–Ό +rag_index ← embeds each chunk via nomic-embed-text GGUF + β”‚ + β–Ό +notcurses.db ← binary vector store (embeddings + text) + + +RUNTIME (each query) +──────────────────────────────────────────────────────────────── +user query + β”‚ + β–Ό +rag_retrieve() ← embeds query, cosine similarity against db + β”‚ ← skips chunks already seen this session + β–Ό +new top-k chunks ← most relevant unseen API fragments + β”‚ + β–Ό +prompt assembly ← system + prior history + new context + query + β”‚ + β–Ό +Qwen3 inference ← <|think|> reasoning + final answer + β”‚ + β–Ό +history ← appended for next turn (KV cache intact) +``` + +--- + +## Files + +| File | Purpose | +|---|---| +| `chunk_headers.cpp` | Parses C/C++ headers into semantic chunks, outputs `.jsonl` | +| `rag_index.cpp` | Reads `.jsonl`, embeds each chunk, saves binary `.db` | +| `rag.hpp` | Single-header C++17 runtime β€” load db, session, retrieve | +| `example.cpp` | Full pipeline wired together, multi-turn query loop | + +--- + +## Dependencies + +- [llama.cpp](https://github.com/ggerganov/llama.cpp) β€” `libllama` + `llama.h` +- A GGUF **inference model** β€” tested with `Qwen3.5-9B-Q4_K_M.gguf` +- A GGUF **embedding model** β€” + `nomic-embed-text-v1.5.Q4_K_M.gguf` + ([download](https://huggingface.co/nomic-ai/nomic-embed-text-v1.5-GGUF)) +- C++17 compiler (gcc 8+, clang 7+, MSVC 2019+) + +--- + +## Build + +```bash +c++ -std=c++17 -o chunk_headers chunk_headers.cpp +c++ -std=c++17 -o rag_index rag_index.cpp -lllama -lm +c++ -std=c++17 -o example example.cpp -lllama -lm +``` + +If llama.cpp is not on your system library path: + +```bash +c++ -std=c++17 -o rag_index rag_index.cpp \ + -I/path/to/llama.cpp/include \ + -L/path/to/llama.cpp/build -lllama -lm +``` + +--- + +## Usage + +### Step 1 β€” Chunk the headers (one-time) + +```bash +./chunk_headers notcurses/include/notcurses/ > chunks.jsonl +``` + +Accepts a single file or a directory (walked recursively). +Multiple paths can be given: + +```bash +./chunk_headers include/foo.h include/bar.h src/examples/ > chunks.jsonl +``` + +Handles `.h`, `.hpp`, `.c`, `.cpp`. Inspect before indexing: + +```bash +head -5 chunks.jsonl | python3 -m json.tool +``` + +### Step 2 β€” Embed and index (one-time) + +```bash +./rag_index \ + --model nomic-embed-text-v1.5.Q4_K_M.gguf \ + --input chunks.jsonl \ + --output notcurses.db +``` + +Takes a few minutes for a large corpus. The `.db` is reusable +until the library changes. + +### Step 3 β€” Run + +```bash +./example \ + --model Qwen3.5-9B-Q4_K_M.gguf \ + --embed nomic-embed-text-v1.5.Q4_K_M.gguf \ + --db notcurses.db +``` + +``` +notcurses expert ready. ctrl+d to quit. + +you: how do I create a plane and render text into it? +assistant: ... + +you: what options does it take? ← follow-up; no repeated context +assistant: ... +``` + +--- + +## Using rag.hpp in your own project + +Single-header, stb-style. In **one** `.cpp` file: + +```cpp +#define RAG_IMPLEMENTATION +#include "rag.hpp" +``` + +All other files that need the types: + +```cpp +#include "rag.hpp" +``` + +### Minimal integration + +```cpp +// startup +RagDB db; +rag_load(db, "notcurses.db"); + +RagSession session; +session.init(db.size(), 8192); // n_chunks, your n_ctx +session.score_threshold = 0.60f; + +// each turn +std::string context = rag_retrieve(db, embed_ctx, embed_model, + user_query, 5, session); +// context is empty string if nothing new/relevant was found +// build prompt with context and hand to your inference context +``` + +### Stateless retrieval (no deduplication) + +```cpp +std::string context = rag_retrieve(db, embed_ctx, embed_model, + user_query, 5); +``` + +### API + +```cpp +// Load .db file (version 2). Returns true on success. +bool rag_load(RagDB &db, const std::string &path); + +// Retrieve with session deduplication + token budget. +// Returns context string ready to inject into prompt. +// Empty string if nothing new or relevant was found. +std::string rag_retrieve(const RagDB &db, + llama_context *embed_ctx, + llama_model *embed_model, + const std::string &query, + int top_k, + RagSession &session); + +// Stateless overload β€” no deduplication. +std::string rag_retrieve(const RagDB &db, + llama_context *embed_ctx, + llama_model *embed_model, + const std::string &query, + int top_k); +``` + +### RagSession fields + +```cpp +struct RagSession { + std::vector seen; // one bit per chunk, sized to db + int tokens_used = 0; // running token estimate + int tokens_max = 0; // your n_ctx ceiling + float score_threshold = 0.60f; // skip weak matches + + void init(int n_chunks, int ctx_size); + void reset(); // start a fresh conversation +}; +``` + +--- + +## Chunking strategy + +`chunk_headers` uses a state machine that keeps each **semantic unit** +together as one chunk: + +- Block comment (`/* ... */`) + following declaration +- `//` line comments + following declaration +- `typedef struct` / `typedef enum` entire body +- Consecutive `#define` macro groups +- Multi-line function signatures + +Example β€” this stays as one chunk: + +```c +// ncplane_create() - create a new plane as a child of 'n'. +// 'nopts' may be NULL for defaults. Returns NULL on error. +struct ncplane* ncplane_create(struct ncplane *n, + const struct ncplane_options *nopts); +``` + +--- + +## Session deduplication + +The KV cache is not cleared between turns, so the model already has +earlier chunks in memory. `RagSession` tracks which chunks have been +injected and skips them on subsequent turns: + +``` +Turn 1: retrieved chunks [42, 17, 83] β†’ all new β†’ inject all +Turn 2: retrieved chunks [42, 55, 17] β†’ 42,17 seen β†’ inject only [55] +Turn 3: retrieved chunks [7, 14, 55] β†’ 55 seen β†’ inject [7, 14] +``` + +Context window grows efficiently β€” no repeated API reference, and the +model remembers everything already seen via the intact KV cache. + +--- + +## Adapting to other libraries + +Change only the input to `chunk_headers`: + +| Library | Input | +|---|---| +| stb (stb_image, stb_truetype ...) | single `.h` file | +| SDL2 / OpenGL / Vulkan | `include/` directory | +| Your own engine | any `.h` / `.hpp` mix | +| Spring / Java | extend chunker for Javadoc + `.java` | + +Re-run steps 1 and 2 to produce a new `.db`. Runtime code unchanged. +Multiple `.db` files can be loaded and queried independently. + +--- + +## .db file format (version 2) + +Variable-length fields β€” no wasted padding. + +``` +Header (16 bytes): + uint32 magic = 0x52414744 ("RAGD") + uint32 version = 2 + uint32 n_chunks + uint32 embed_dim + +Per chunk: + uint32 text_len + char[] text (text_len bytes, no null) + uint16 source_len + char[] source (source_len bytes, no null) + uint8 type_len + char[] type (type_len bytes, no null) + float[] embedding (embed_dim Γ— 4 bytes) +``` + +--- + +## GPU memory + +On an 8 GB GPU with `Qwen3.5-9B-Q4_K_M`: + +| Component | VRAM | +|---|---| +| Inference model (Q4_K_M 9B) | ~5.5 GB | +| Embedding model (nomic Q4) | ~0.3 GB | +| KV cache (8k ctx, Q4_0 K/V) | ~0.5 GB | +| **Total** | **~6.3 GB** | + +--- + +## Qwen3 thinking mode + +The model emits `<|think|>...<|/think|>` before its answer. +`example.cpp` strips this with `strip_think()` before printing. +The think block improves RAG quality β€” the model explicitly reasons +over injected context chunks before answering. + +To expose reasoning (useful for debugging retrieval quality), remove +the `strip_think()` call and print `raw` directly. diff --git a/llama/chunk_headers.cpp b/llama/chunk_headers.cpp new file mode 100644 index 0000000..51ecd66 --- /dev/null +++ b/llama/chunk_headers.cpp @@ -0,0 +1,311 @@ +/* + * chunk_headers.cpp + * + * Smart chunker for C/C++ headers β€” keeps semantic units together: + * - block comment + following declaration/function + * - struct/enum/typedef blocks + * - grouped #define macros + * - standalone inline-commented declarations + * + * Output: one chunk per line in a .jsonl file: + * {"source":"notcurses.h","type":"function","text":"..."} + * + * Build: c++ -std=c++17 -o chunk_headers chunk_headers.cpp + * Usage: ./chunk_headers notcurses/include/notcurses/notcurses.h > chunks.jsonl + * ./chunk_headers dir/ > chunks.jsonl + */ + +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +/* ── tunables ──────────────────────────────────────────────── */ +static constexpr size_t MIN_CHUNK = 40; /* ignore tiny fragments */ +/* ─────────────────────────────────────────────────────────── */ + +enum class ChunkType { + Function, Struct, Enum, Typedef, Defines, Other +}; + +static std::string type_name(ChunkType t) { + switch (t) { + case ChunkType::Function: return "function"; + case ChunkType::Struct: return "struct"; + case ChunkType::Enum: return "enum"; + case ChunkType::Typedef: return "typedef"; + case ChunkType::Defines: return "defines"; + default: return "other"; + } +} + +/* ── helpers ───────────────────────────────────────────────── */ + +static bool starts_with(const std::string &s, const std::string &prefix) { + return s.size() >= prefix.size() && + s.compare(0, prefix.size(), prefix) == 0; +} + +static bool is_blank(const std::string &s) { + for (char c : s) if (!isspace((unsigned char)c)) return false; + return true; +} + +static std::string json_escape(const std::string &in) { + std::string out; + out.reserve(in.size() + 32); + for (unsigned char c : in) { + if (c == '"') { out += "\\\""; } + else if (c == '\\') { out += "\\\\"; } + else if (c == '\n') { out += "\\n"; } + else if (c == '\r') { /* skip */ } + else if (c == '\t') { out += "\\t"; } + else if (c < 0x20) { /* skip */ } + else { out += c; } + } + return out; +} + +static void emit_chunk(const std::string &source, ChunkType type, + const std::string &text) { + if (text.size() < MIN_CHUNK) return; + + /* trim trailing newlines */ + size_t end = text.size(); + while (end > 0 && (text[end-1] == '\n' || text[end-1] == '\r')) --end; + if (end < MIN_CHUNK) return; + + std::cout << "{\"source\":\"" << json_escape(source) + << "\",\"type\":\"" << type_name(type) + << "\",\"text\":\"" << json_escape(text.substr(0, end)) + << "\"}\n"; +} + +/* ── state machine ─────────────────────────────────────────── */ + +enum class State { + Idle, BlockComment, LineComment, Declaration, Struct, Defines +}; + +static void process_file(const fs::path &path) { + std::ifstream f(path); + if (!f) { std::cerr << "cannot open: " << path << "\n"; return; } + + const std::string source = path.filename().string(); + + State state = State::Idle; + std::string chunk; + ChunkType chunk_type = ChunkType::Other; + int brace_depth = 0; + int paren_depth = 0; + int define_count = 0; + + auto flush = [&](ChunkType t) { + emit_chunk(source, t, chunk); + chunk.clear(); + state = State::Idle; + brace_depth = 0; + paren_depth = 0; + }; + + std::string line; + while (std::getline(f, line)) { + /* trim trailing CR */ + if (!line.empty() && line.back() == '\r') line.pop_back(); + + /* find first non-whitespace for prefix checks */ + size_t trim_pos = 0; + while (trim_pos < line.size() && + (line[trim_pos] == ' ' || line[trim_pos] == '\t')) ++trim_pos; + const std::string trimmed = line.substr(trim_pos); + + /* ── #define handling ─────────────────────────────────── */ + if (starts_with(trimmed, "#define ")) { + if (state == State::BlockComment || state == State::LineComment) { + chunk += line + "\n"; + state = State::Defines; + define_count = 1; + } else if (state == State::Defines) { + chunk += line + "\n"; + define_count++; + } else { + if (chunk.size() >= MIN_CHUNK) emit_chunk(source, chunk_type, chunk); + chunk.clear(); + chunk += line + "\n"; + state = State::Defines; + define_count = 1; + } + continue; + } + + /* non-define while in define group */ + if (state == State::Defines) { + flush(ChunkType::Defines); + define_count = 0; + /* fall through to process this line normally */ + } + + /* ── block comment start ──────────────────────────────── */ + if ((starts_with(trimmed, "/*") || starts_with(trimmed, "/**")) && + state == State::Idle) { + if (chunk.size() >= MIN_CHUNK) emit_chunk(source, chunk_type, chunk); + chunk.clear(); + chunk_type = ChunkType::Other; + chunk += line + "\n"; + state = (trimmed.find("*/", 2) != std::string::npos) + ? State::LineComment + : State::BlockComment; + continue; + } + + /* ── inside block comment ─────────────────────────────── */ + if (state == State::BlockComment) { + chunk += line + "\n"; + if (trimmed.find("*/") != std::string::npos) + state = State::LineComment; + continue; + } + + /* ── // line comment ──────────────────────────────────── */ + if (starts_with(trimmed, "//")) { + if (state == State::Idle) { + if (chunk.size() >= MIN_CHUNK) emit_chunk(source, chunk_type, chunk); + chunk.clear(); + chunk += line + "\n"; + state = State::LineComment; + } else if (state == State::LineComment) { + chunk += line + "\n"; + } + continue; + } + + /* ── blank line ───────────────────────────────────────── */ + if (is_blank(trimmed)) { + if (state == State::LineComment) + flush(ChunkType::Other); + else if (state == State::Idle && chunk.size() >= MIN_CHUNK) + flush(chunk_type); + continue; + } + + /* ── skip preprocessor noise ──────────────────────────── */ + if (starts_with(trimmed, "#ifndef") || starts_with(trimmed, "#ifdef") || + starts_with(trimmed, "#endif") || starts_with(trimmed, "#pragma") || + starts_with(trimmed, "#include")) { + if (state == State::LineComment || state == State::BlockComment) { + chunk.clear(); + state = State::Idle; + } + continue; + } + + /* ── typedef struct / enum start ─────────────────────── */ + if ((starts_with(trimmed, "typedef struct") || + starts_with(trimmed, "typedef enum") || + starts_with(trimmed, "struct ") || + starts_with(trimmed, "enum ")) && + (state == State::Idle || state == State::LineComment)) { + + if (state == State::Idle && chunk.size() >= MIN_CHUNK) + emit_chunk(source, chunk_type, chunk); + + /* preserve any comment already in chunk */ + if (state == State::Idle) chunk.clear(); + + chunk += line + "\n"; + chunk_type = starts_with(trimmed, "typedef") ? ChunkType::Typedef + : starts_with(trimmed, "enum ") ? ChunkType::Enum + : ChunkType::Struct; + state = State::Struct; + for (char c : line) { + if (c == '{') ++brace_depth; + if (c == '}') --brace_depth; + } + if (brace_depth <= 0 && line.find(';') != std::string::npos) + flush(chunk_type); + continue; + } + + /* ── inside struct/enum body ──────────────────────────── */ + if (state == State::Struct) { + chunk += line + "\n"; + for (char c : line) { + if (c == '{') ++brace_depth; + if (c == '}') --brace_depth; + } + if (brace_depth <= 0 && line.find(';') != std::string::npos) + flush(chunk_type); + continue; + } + + /* ── function / other declaration ────────────────────── */ + if (state == State::LineComment || state == State::Idle) { + if (state == State::Idle && chunk.size() >= MIN_CHUNK) { + emit_chunk(source, chunk_type, chunk); + chunk.clear(); + } + chunk += line + "\n"; + chunk_type = ChunkType::Function; + state = State::Declaration; + for (char c : line) { + if (c == '(') ++paren_depth; + if (c == ')') --paren_depth; + } + if (paren_depth <= 0 && line.find(';') != std::string::npos) + flush(ChunkType::Function); + continue; + } + + /* ── multi-line declaration ───────────────────────────── */ + if (state == State::Declaration) { + chunk += line + "\n"; + for (char c : line) { + if (c == '(') ++paren_depth; + if (c == ')') --paren_depth; + } + if (paren_depth <= 0 && line.find(';') != std::string::npos) + flush(ChunkType::Function); + continue; + } + } + + /* flush remainder */ + if (chunk.size() >= MIN_CHUNK) emit_chunk(source, chunk_type, chunk); +} + +/* ── directory walker ──────────────────────────────────────── */ + +static void process_path(const fs::path &path) { + if (fs::is_directory(path)) { + /* sorted for deterministic output */ + std::vector entries; + for (auto &e : fs::recursive_directory_iterator(path)) + entries.push_back(e.path()); + std::sort(entries.begin(), entries.end()); + for (auto &e : entries) { + if (!fs::is_regular_file(e)) continue; + auto ext = e.extension().string(); + if (ext == ".h" || ext == ".hpp" || ext == ".c" || ext == ".cpp") + process_file(e); + } + } else if (fs::is_regular_file(path)) { + process_file(path); + } +} + +/* ── main ──────────────────────────────────────────────────── */ + +int main(int argc, char **argv) { + if (argc < 2) { + std::cerr << "usage: " << argv[0] + << " [header2.h ...]\n"; + return 1; + } + for (int i = 1; i < argc; i++) + process_path(fs::path(argv[i])); + return 0; +} diff --git a/llama/llama-sb.cpp b/llama/llama-sb.cpp index 4fde33d..05463fb 100644 --- a/llama/llama-sb.cpp +++ b/llama/llama-sb.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include "ggml-cuda.h" @@ -63,8 +64,12 @@ Llama::Llama() : _n_past(0), _is_gemma4(false), _seed(LLAMA_DEFAULT_SEED) { - llama_log_set([](enum ggml_log_level level, const char * text, void *user_data) { + llama_log_set([](enum ggml_log_level level, const char *text, void *user_data) { Llama *llama = (Llama *)user_data; + if (level == GGML_LOG_LEVEL_ERROR && llama->_last_error.empty()) { + // remember the first error message + llama->_last_error = text; + } if (level > llama->_log_level) { fprintf(stderr, "LLAMA: %s", text); } @@ -135,7 +140,7 @@ void Llama::reset() { } } -bool Llama::construct(string model_path, int n_ctx, int n_batch, int n_gpu_layers, int log_level) { +bool Llama::load_model(string model_path, int n_ctx, int n_batch, int n_gpu_layers, int log_level) { ggml_backend_load_all(); llama_model_params mparams = llama_model_default_params(); @@ -143,11 +148,12 @@ bool Llama::construct(string model_path, int n_ctx, int n_batch, int n_gpu_layer mparams.n_gpu_layers = n_gpu_layers; } + _last_error.clear(); _log_level = log_level; _n_gpu_layers = n_gpu_layers; _model = llama_model_load_from_file(model_path.c_str(), mparams); if (!_model) { - _last_error = "Failed to load model"; + set_last_error("Load model"); } else { llama_context_params cparams = llama_context_default_params(); cparams.n_ctx = n_ctx; @@ -166,7 +172,7 @@ bool Llama::construct(string model_path, int n_ctx, int n_batch, int n_gpu_layer _ctx = llama_init_from_model(_model, cparams); if (!_ctx) { - _last_error = "Failed to create context"; + set_last_error("Create context"); } else { _vocab = llama_model_get_vocab(_model); } @@ -177,125 +183,91 @@ bool Llama::construct(string model_path, int n_ctx, int n_batch, int n_gpu_layer return _last_error.empty(); } -void Llama::set_grammar(const string &src, const string &root) { - _grammar_src = src; - _grammar_root = root; -} +bool Llama::load_embedding_model(string model_path) { + ggml_backend_load_all(); -bool Llama::configure_sampler() { - auto sparams = llama_sampler_chain_default_params(); - sparams.no_perf = false; - llama_sampler *chain = llama_sampler_chain_init(sparams); + llama_model_params mparams = llama_model_default_params(); + mparams.n_gpu_layers = 99; - if (!_grammar_src.empty()) { - llama_sampler *grammar = llama_sampler_init_grammar(_vocab, _grammar_src.c_str(), _grammar_root.c_str()); - if (!grammar) { - _last_error = "failed to initialize grammar sampler"; - return false; - } - llama_sampler_chain_add(chain, grammar); - } - if (_penalty_last_n != 0 && _penalty_repeat != 1.0f) { - auto penalties = llama_sampler_init_penalties(_penalty_last_n, _penalty_repeat, _penalty_freq, _penalty_present); - llama_sampler_chain_add(chain, penalties); - } - if (_temperature <= 0.0f) { - llama_sampler_chain_add(chain, llama_sampler_init_greedy()); + _last_error.clear(); + _model = llama_model_load_from_file(model_path.c_str(), mparams); + if (!_model) { + set_last_error("Load model"); } else { - if (_top_k > 0) { - llama_sampler_chain_add(chain, llama_sampler_init_top_k(_top_k)); - } - if (_top_p < 1.0f || _min_p > 0.0f) { - llama_sampler_chain_add(chain, llama_sampler_init_top_p(_top_p, 1)); - } - if (_min_p > 0.0f) { - llama_sampler_chain_add(chain, llama_sampler_init_min_p(_min_p, 1)); + llama_context_params cparams = llama_context_default_params(); + cparams.n_ctx = 512; + cparams.n_batch = 512; + cparams.embeddings = true; + cparams.pooling_type = LLAMA_POOLING_TYPE_MEAN; + + _ctx = llama_init_from_model(_model, cparams); + if (!_ctx) { + set_last_error("Create context"); } - llama_sampler_chain_add(chain, llama_sampler_init_temp(_temperature)); - llama_sampler_chain_add(chain, llama_sampler_init_dist(_seed)); - } - if (_sampler) { - llama_sampler_free(_sampler); } - _sampler = chain; - return true; -} -vector Llama::tokenize(const string &prompt) { - vector result; + return _last_error.empty(); +} - int n_prompt = -llama_tokenize(_vocab, prompt.c_str(), prompt.size(), nullptr, 0, true, true); - if (n_prompt <= 0) { - _last_error = "Failed to tokenize prompt"; - } else { - result.reserve(n_prompt); - result.resize(n_prompt); - if (llama_tokenize(_vocab, prompt.c_str(), prompt.size(), - result.data(), n_prompt, true, true) < 0) { - _last_error = "Failed to tokenize prompt"; - } - } - return result; +int Llama::get_embed_dim() { + return _model != nullptr ? llama_model_n_embd(_model) : 0; } -// Makes space in the context for n_tokens by removing old tokens if necessary -// Returns true if successful, false if impossible to make space -// -// Strategies: -// - If enough space exists, does nothing -// - If n_tokens > n_ctx, fails (impossible to fit) -// - Otherwise, removes oldest tokens to make room -// -// Parameters: -// n_tokens - Number of tokens we need space for -// keep_min - Minimum tokens to keep (e.g., system prompt), default 0 -// -bool Llama::make_space_for_tokens(int n_tokens, int keep_min) { - int n_ctx = llama_n_ctx(_ctx); - if (n_tokens > n_ctx) { - _last_error = "Too many tokens, increase context size (n_ctx)"; +bool Llama::embed_text(const std::string &text, std::vector &out, int embed_dim) { + std::string prefixed = "search_document: " + text; + + vector tokens = tokenize(prefixed); + if (tokens.size() == 0) { return false; } - llama_memory_t mem = llama_get_memory(_ctx); - - // Get current position range - llama_pos pos_min = llama_memory_seq_pos_min(mem, 0); - llama_pos pos_max = llama_memory_seq_pos_max(mem, 0); - - // Empty memory - nothing to do - if (pos_max < 0) { - return true; + // truncate to context window + int n_ctx = llama_n_ctx(_ctx); + int n = tokens.size(); + if (n > n_ctx) { + _last_error = std::format("warning: chunk truncated {} -> {} tokens ", n, n_ctx); + n = n_ctx; + tokens.resize(n); } - int current_used = pos_max - pos_min + 1; - int space_needed = n_tokens; - int space_available = n_ctx - current_used; + llama_memory_clear(llama_get_memory(_ctx), true); - // Already have enough space - if (space_available >= space_needed) { - return true; + if (!batch_decode_tokens(tokens)) { + return false; } - // Calculate how many tokens to remove - int tokens_to_remove = space_needed - space_available; + float *emb = llama_get_embeddings_seq(_ctx, 0); + if (!emb) { + emb = llama_get_embeddings_ith(_ctx, n - 1); + } - // Can't remove more than we have (minus keep_min) - int removable = current_used - keep_min; - if (tokens_to_remove > removable) { - _last_error = "Can't make enough space while keeping keep_min tokens"; + if (!emb) { + _last_error = "no embedding returned\n"; return false; } - // Remove oldest tokens (from pos_min to pos_min + tokens_to_remove) - llama_memory_seq_rm(mem, 0, pos_min, pos_min + tokens_to_remove); + out.assign(emb, emb + embed_dim); - // Shift remaining tokens down - llama_memory_seq_add(mem, 0, pos_min + tokens_to_remove, -1, -tokens_to_remove); + /* L2 normalize */ + float norm = 0.0f; + for (float v : out) { + norm += v * v; + } + norm = std::sqrt(norm); + if (norm > 1e-9f) { + for (float &v : out) { + v /= norm; + } + } return true; } +void Llama::set_grammar(const string &src, const string &root) { + _grammar_src = src; + _grammar_root = root; +} + bool Llama::add_message(LlamaIter &iter, const string &role, const string &content) { llama_chat_message message = {role.c_str(), content.c_str()}; int buf_size = 2 * (int)(role.size() + content.size() + 64); @@ -342,16 +314,8 @@ bool Llama::add_message(LlamaIter &iter, const string &role, const string &conte } // batch decode tokens - uint32_t n_batch = llama_n_batch(_ctx); - for (size_t i = 0; i < prompt_tokens.size(); i += n_batch) { - size_t batch_size = std::min((size_t)n_batch, prompt_tokens.size() - i); - llama_batch batch = llama_batch_get_one(prompt_tokens.data() + i, batch_size); - int result = llama_decode(_ctx, batch); - if (result != 0) { - _last_error = std::format("Failed to decode batch. position:{} error:{} [size:{}, past:{}]", - i, result, prompt_tokens.size(), _n_past); - return false; - } + if (!batch_decode_tokens(prompt_tokens)) { + return false; } // handle encoder models @@ -377,87 +341,6 @@ bool Llama::add_message(LlamaIter &iter, const string &role, const string &conte return true; } -bool Llama::ends_with_sentence_boundary(const string &text) { - if (text.empty()) { - return false; - } - - // Get last few characters (in case of whitespace after punctuation) - size_t check_len = std::min(text.length(), (size_t)5); - std::string ending = text.substr(text.length() - check_len); - - // Check for various sentence endings - // Period followed by space or end - if (ending.find(". ") != std::string::npos || - ending.back() == '.') { - return true; - } - - // Exclamation mark - if (ending.find("! ") != std::string::npos || - ending.back() == '!') { - return true; - } - - // Question mark - if (ending.find("? ") != std::string::npos || - ending.back() == '?') { - return true; - } - - // Newline (paragraph break) - if (ending.find('\n') != std::string::npos) { - return true; - } - - // Quote followed by period: "something." - if (ending.find(".\"") != std::string::npos || - ending.find("!\"") != std::string::npos || - ending.find("?\"") != std::string::npos) { - return true; - } - - return false; -} - -string Llama::token_to_string(LlamaIter &iter, llama_token tok) { - string result; - char buf[512]; - int n = llama_token_to_piece(_vocab, tok, buf, sizeof(buf), 0, false); - if (n > 0) { - // detect repetition - if (iter._last_word == buf) { - if (++iter._repetition_count == MAX_REPEAT) { - iter._has_next = false; - } - } else { - iter._repetition_count = 0; - iter._last_word = buf; - } - - result.append(buf, n); - - // detect end of max-tokens - if (++iter._tokens_generated > _max_tokens && ends_with_sentence_boundary(result)) { - iter._has_next = false; - } - - // detect stop words - if (iter._has_next) { - for (const auto &stop : _stop_sequences) { - size_t pos = result.find(stop); - if (pos != std::string::npos) { - // found stop sequence - truncate and signal end - result = result.substr(0, pos); - iter._has_next = false; - break; - } - } - } - } - return result; -} - string Llama::next(LlamaIter &iter) { if (!iter._has_next) { _last_error = "Iteration beyond end of stream"; @@ -573,3 +456,223 @@ LlamaMemoryInfo Llama::memory_info() { return info; } +bool Llama::batch_decode_tokens(vector &tokens) { + uint32_t n_batch = llama_n_batch(_ctx); + for (size_t i = 0; i < tokens.size(); i += n_batch) { + size_t batch_size = std::min((size_t)n_batch, tokens.size() - i); + llama_batch batch = llama_batch_get_one(tokens.data() + i, batch_size); + int result = llama_decode(_ctx, batch); + if (result != 0) { + _last_error = std::format("Failed to decode batch. position:{} error:{} [size:{}, past:{}]", + i, result, tokens.size(), _n_past); + return false; + } + } + return true; +} + +bool Llama::configure_sampler() { + auto sparams = llama_sampler_chain_default_params(); + sparams.no_perf = false; + llama_sampler *chain = llama_sampler_chain_init(sparams); + + if (!_grammar_src.empty()) { + llama_sampler *grammar = llama_sampler_init_grammar(_vocab, _grammar_src.c_str(), _grammar_root.c_str()); + if (!grammar) { + _last_error = "failed to initialize grammar sampler"; + return false; + } + llama_sampler_chain_add(chain, grammar); + } + if (_penalty_last_n != 0 && _penalty_repeat != 1.0f) { + auto penalties = llama_sampler_init_penalties(_penalty_last_n, _penalty_repeat, _penalty_freq, _penalty_present); + llama_sampler_chain_add(chain, penalties); + } + if (_temperature <= 0.0f) { + llama_sampler_chain_add(chain, llama_sampler_init_greedy()); + } else { + if (_top_k > 0) { + llama_sampler_chain_add(chain, llama_sampler_init_top_k(_top_k)); + } + if (_top_p < 1.0f || _min_p > 0.0f) { + llama_sampler_chain_add(chain, llama_sampler_init_top_p(_top_p, 1)); + } + if (_min_p > 0.0f) { + llama_sampler_chain_add(chain, llama_sampler_init_min_p(_min_p, 1)); + } + llama_sampler_chain_add(chain, llama_sampler_init_temp(_temperature)); + llama_sampler_chain_add(chain, llama_sampler_init_dist(_seed)); + } + if (_sampler) { + llama_sampler_free(_sampler); + } + _sampler = chain; + return true; +} + +bool Llama::ends_with_sentence_boundary(const string &text) { + if (text.empty()) { + return false; + } + + // Get last few characters (in case of whitespace after punctuation) + size_t check_len = std::min(text.length(), (size_t)5); + std::string ending = text.substr(text.length() - check_len); + + // Check for various sentence endings + // Period followed by space or end + if (ending.find(". ") != std::string::npos || + ending.back() == '.') { + return true; + } + + // Exclamation mark + if (ending.find("! ") != std::string::npos || + ending.back() == '!') { + return true; + } + + // Question mark + if (ending.find("? ") != std::string::npos || + ending.back() == '?') { + return true; + } + + // Newline (paragraph break) + if (ending.find('\n') != std::string::npos) { + return true; + } + + // Quote followed by period: "something." + if (ending.find(".\"") != std::string::npos || + ending.find("!\"") != std::string::npos || + ending.find("?\"") != std::string::npos) { + return true; + } + + return false; +} + +// Makes space in the context for n_tokens by removing old tokens if necessary +// Returns true if successful, false if impossible to make space +// +// Strategies: +// - If enough space exists, does nothing +// - If n_tokens > n_ctx, fails (impossible to fit) +// - Otherwise, removes oldest tokens to make room +// +// Parameters: +// n_tokens - Number of tokens we need space for +// keep_min - Minimum tokens to keep (e.g., system prompt), default 0 +// +bool Llama::make_space_for_tokens(int n_tokens, int keep_min) { + int n_ctx = llama_n_ctx(_ctx); + if (n_tokens > n_ctx) { + _last_error = "Too many tokens, increase context size (n_ctx)"; + return false; + } + + llama_memory_t mem = llama_get_memory(_ctx); + + // Get current position range + llama_pos pos_min = llama_memory_seq_pos_min(mem, 0); + llama_pos pos_max = llama_memory_seq_pos_max(mem, 0); + + // Empty memory - nothing to do + if (pos_max < 0) { + return true; + } + + int current_used = pos_max - pos_min + 1; + int space_needed = n_tokens; + int space_available = n_ctx - current_used; + + // Already have enough space + if (space_available >= space_needed) { + return true; + } + + // Calculate how many tokens to remove + int tokens_to_remove = space_needed - space_available; + + // Can't remove more than we have (minus keep_min) + int removable = current_used - keep_min; + if (tokens_to_remove > removable) { + _last_error = "Can't make enough space while keeping keep_min tokens"; + return false; + } + + // Remove oldest tokens (from pos_min to pos_min + tokens_to_remove) + llama_memory_seq_rm(mem, 0, pos_min, pos_min + tokens_to_remove); + + // Shift remaining tokens down + llama_memory_seq_add(mem, 0, pos_min + tokens_to_remove, -1, -tokens_to_remove); + + return true; +} + +vector Llama::tokenize(const string &prompt) { + vector result; + + int n_prompt = -llama_tokenize(_vocab, prompt.c_str(), prompt.size(), nullptr, 0, true, true); + if (n_prompt <= 0) { + _last_error = "Failed to tokenize prompt"; + } else { + result.reserve(n_prompt); + result.resize(n_prompt); + if (llama_tokenize(_vocab, prompt.c_str(), prompt.size(), + result.data(), n_prompt, true, true) < 0) { + _last_error = "Failed to tokenize prompt"; + } + } + return result; +} + +string Llama::token_to_string(LlamaIter &iter, llama_token tok) { + string result; + char buf[512]; + int n = llama_token_to_piece(_vocab, tok, buf, sizeof(buf), 0, false); + if (n > 0) { + // detect repetition + if (iter._last_word == buf) { + if (++iter._repetition_count == MAX_REPEAT) { + iter._has_next = false; + } + } else { + iter._repetition_count = 0; + iter._last_word = buf; + } + + result.append(buf, n); + + // detect end of max-tokens + if (++iter._tokens_generated > _max_tokens && ends_with_sentence_boundary(result)) { + iter._has_next = false; + } + + // detect stop words + if (iter._has_next) { + for (const auto &stop : _stop_sequences) { + size_t pos = result.find(stop); + if (pos != std::string::npos) { + // found stop sequence - truncate and signal end + result = result.substr(0, pos); + iter._has_next = false; + break; + } + } + } + } + return result; +} + +void Llama::set_last_error(const char *message) { + if (!_last_error.empty()) { + if (_last_error.back() == '\n') { + _last_error.pop_back(); + } + _last_error = std::format("{}: {}", message, _last_error); + } else { + _last_error = std::format("{} failed", message); + } +} diff --git a/llama/llama-sb.h b/llama/llama-sb.h index 79dc2e3..99a3864 100644 --- a/llama/llama-sb.h +++ b/llama/llama-sb.h @@ -68,7 +68,8 @@ struct Llama { ~Llama(); // init - bool construct(string model_path, int n_ctx, int n_batch, int n_gpu_layers, int log_level); + bool load_model(string model_path, int n_ctx, int n_batch, int n_gpu_layers, int log_level); + bool load_embedding_model(string model_path); // generation bool add_message(LlamaIter &iter, const string &role, const string &content); @@ -98,13 +99,18 @@ struct Llama { // memory info LlamaMemoryInfo memory_info(); + // rag support + bool embed_text(const std::string &text, std::vector &out, int embed); + int get_embed_dim(); + private: - bool ends_with_sentence_boundary(const string &out); + bool batch_decode_tokens(vector &tokens); bool configure_sampler(); + bool ends_with_sentence_boundary(const string &out); bool make_space_for_tokens(int n_tokens, int keep_min); vector tokenize(const string &prompt); string token_to_string(LlamaIter &iter, llama_token tok); - bool encode(const string &role, const string &content, bool add_assistant_prompt) ; + void set_last_error(const char *message); llama_model *_model; llama_context *_ctx; diff --git a/llama/llama.cpp b/llama/llama.cpp index 5d44db6..7f3f843 160000 --- a/llama/llama.cpp +++ b/llama/llama.cpp @@ -1 +1 @@ -Subproject commit 5d44db60089b0381cdbf7c45ce9ded43fc0c7f4c +Subproject commit 7f3f843c31cd32dc4adc10b393342dfee071c332 diff --git a/llama/main.cpp b/llama/main.cpp index aa235d9..b78d015 100644 --- a/llama/main.cpp +++ b/llama/main.cpp @@ -469,7 +469,7 @@ static int cmd_create_llama(int argc, slib_par_t *params, var_t *retval) { auto n_log_level = get_param_int(argc, params, 4, GGML_LOG_LEVEL_CONT); int id = ++g_nextId; Llama &llama = g_llama[id]; - if (llama.construct(model, n_ctx, n_batch, n_gpu_layers, n_log_level)) { + if (llama.load_model(model, n_ctx, n_batch, n_gpu_layers, n_log_level)) { map_init_id(retval, id, CLASS_ID_LLAMA); v_create_callback(retval, "add_stop", cmd_llama_add_stop); v_create_callback(retval, "add_message", cmd_llama_add_message); diff --git a/llama/rag.hpp b/llama/rag.hpp new file mode 100644 index 0000000..3e133da --- /dev/null +++ b/llama/rag.hpp @@ -0,0 +1,328 @@ +/* + * rag.hpp + * + * Single-header C++ RAG runtime library. + * No fixed limits on chunk count, chunk length, or embedding dimension. + * Includes RagSession for deduplication across turns. + * + * Usage (in ONE .cpp file): + * #define RAG_IMPLEMENTATION + * #include "rag.hpp" + * + * All other files: + * #include "rag.hpp" + * + * Db format version: 2 (written by rag_index.cpp) + */ + +#pragma once + +#include "llama.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* ═══════════════════════════════════════════════════════════ + * Data structures + * ═══════════════════════════════════════════════════════════ */ + +struct RagChunk { + std::string text; + std::string source; + std::string type; + std::vector embedding; +}; + +struct RagDB { + std::vector chunks; + int embed_dim = 0; + + int size() const { return (int)chunks.size(); } + bool empty() const { return chunks.empty(); } +}; + +/* ── per-session deduplication + token budget ──────────────── */ +struct RagSession { + std::vector seen; /* sized to db.size() on init */ + int tokens_used = 0; + int tokens_max = 0; /* set to your n_ctx */ + float score_threshold = 0.60f; /* skip weak matches */ + + void init(int n_chunks, int ctx_size) { + seen.assign(n_chunks, false); + tokens_used = 0; + tokens_max = ctx_size; + } + + void reset() { + std::fill(seen.begin(), seen.end(), false); + tokens_used = 0; + } + + bool is_seen(int idx) const { return idx < (int)seen.size() && seen[idx]; } + void mark(int idx) { if (idx < (int)seen.size()) seen[idx] = true; } + + /* rough token estimate: 1 token β‰ˆ 4 chars */ + bool budget_ok(const std::string &text) const { + return tokens_max == 0 || + (tokens_used + (int)text.size() / 4) < (int)(tokens_max * 0.85f); + } + + void charge(const std::string &text) { + tokens_used += (int)text.size() / 4; + } +}; + +/* ═══════════════════════════════════════════════════════════ + * API declarations + * ═══════════════════════════════════════════════════════════ */ + +/* Load .db file produced by rag_index.cpp (version 2). + * Returns true on success. */ +bool rag_load(RagDB &db, const std::string &path); + +/* + * Embed query, score all chunks, inject top_k unseen results into out. + * Skips chunks already in session.seen and below session.score_threshold. + * Updates session.seen and session.tokens_used. + * Returns the context string (empty if nothing retrieved). + */ +std::string rag_retrieve(const RagDB &db, + llama_context *embed_ctx, + llama_model *embed_model, + const std::string &query, + int top_k, + RagSession &session); + +/* + * Stateless overload β€” no deduplication, no budget tracking. + * Useful for one-shot queries or testing retrieval quality. + */ +std::string rag_retrieve(const RagDB &db, + llama_context *embed_ctx, + llama_model *embed_model, + const std::string &query, + int top_k); + +/* ═══════════════════════════════════════════════════════════ + * Implementation + * ═══════════════════════════════════════════════════════════ */ +#ifdef RAG_IMPLEMENTATION + +/* ── db load ───────────────────────────────────────────────── */ +bool rag_load(RagDB &db, const std::string &path) { + std::ifstream f(path, std::ios::binary); + if (!f) { std::cerr << "rag_load: cannot open " << path << "\n"; return false; } + + auto read32 = [&]() -> uint32_t { + uint32_t v = 0; f.read((char*)&v, 4); return v; + }; + auto read16 = [&]() -> uint16_t { + uint16_t v = 0; f.read((char*)&v, 2); return v; + }; + auto read8 = [&]() -> uint8_t { + uint8_t v = 0; f.read((char*)&v, 1); return v; + }; + auto readstr = [&](size_t len) -> std::string { + std::string s(len, '\0'); + f.read(&s[0], (std::streamsize)len); + return s; + }; + + uint32_t magic = read32(); + uint32_t version = read32(); + uint32_t n = read32(); + uint32_t edim = read32(); + + if (magic != 0x52414744) { + std::cerr << "rag_load: bad magic\n"; return false; + } + if (version != 2) { + std::cerr << "rag_load: unsupported version " << version + << " (expected 2)\n"; return false; + } + + db.embed_dim = (int)edim; + db.chunks.resize(n); + + for (uint32_t i = 0; i < n; i++) { + RagChunk &c = db.chunks[i]; + + uint32_t text_len = read32(); + c.text = readstr(text_len); + + uint16_t src_len = read16(); + c.source = readstr(src_len); + + uint8_t type_len = read8(); + c.type = readstr(type_len); + + c.embedding.resize(edim); + f.read((char*)c.embedding.data(), + (std::streamsize)(edim * sizeof(float))); + } + + if (!f) { std::cerr << "rag_load: read error\n"; return false; } + + std::cerr << "rag: loaded " << db.chunks.size() + << " chunks (dim=" << db.embed_dim + << ") from " << path << "\n"; + return true; +} + +/* ── embed query ───────────────────────────────────────────── */ +static bool rag_embed_query(llama_context *ctx, + llama_model *model, + const std::string &query, + std::vector &out, + int embed_dim) { + std::string prefixed = "search_query: " + query; + + int n = -llama_tokenize(model, + prefixed.c_str(), (int)prefixed.size(), + nullptr, 0, true, true); + if (n <= 0) return false; + + std::vector tokens(n); + llama_tokenize(model, + prefixed.c_str(), (int)prefixed.size(), + tokens.data(), n, true, true); + + int n_ctx = llama_n_ctx(ctx); + if (n > n_ctx) n = n_ctx; + + llama_kv_cache_clear(ctx); + + llama_batch batch = llama_batch_init(n, 0, 1); + for (int i = 0; i < n; i++) { + llama_seq_id seq = 0; + llama_batch_add(batch, tokens[i], i, &seq, 1, i == n - 1); + } + + if (llama_decode(ctx, batch) != 0) { + llama_batch_free(batch); + return false; + } + + float *emb = llama_get_embeddings_seq(ctx, 0); + if (!emb) emb = llama_get_embeddings_ith(ctx, n - 1); + if (!emb) { llama_batch_free(batch); return false; } + + out.assign(emb, emb + embed_dim); + + float norm = 0.0f; + for (float v : out) norm += v * v; + norm = std::sqrt(norm); + if (norm > 1e-9f) + for (float &v : out) v /= norm; + + llama_batch_free(batch); + return true; +} + +/* ── cosine similarity (vectors already L2-normalized) ─────── */ +static float rag_cosine(const std::vector &a, + const std::vector &b) { + float dot = 0.0f; + size_t n = std::min(a.size(), b.size()); + for (size_t i = 0; i < n; i++) dot += a[i] * b[i]; + return dot; +} + +/* ── build context string from ranked results ──────────────── */ +static std::string rag_build_context( + const RagDB &db, + const std::vector &indices, + const std::vector &scores) { + std::ostringstream out; + for (size_t i = 0; i < indices.size(); i++) { + const RagChunk &c = db.chunks[indices[i]]; + out << "// source: " << c.source + << " [" << c.type << "]" + << " (score: " << scores[i] << ")\n" + << c.text << "\n---\n"; + } + return out.str(); +} + +/* ── retrieve with session ─────────────────────────────────── */ +std::string rag_retrieve(const RagDB &db, + llama_context *embed_ctx, + llama_model *embed_model, + const std::string &query, + int top_k, + RagSession &session) { + if (db.empty()) return {}; + + std::vector qvec; + if (!rag_embed_query(embed_ctx, embed_model, query, qvec, db.embed_dim)) + return {}; + + /* score all chunks */ + std::vector order(db.size()); + std::iota(order.begin(), order.end(), 0); + std::vector scores(db.size()); + for (int i = 0; i < db.size(); i++) + scores[i] = rag_cosine(qvec, db.chunks[i].embedding); + + std::sort(order.begin(), order.end(), + [&](int a, int b){ return scores[a] > scores[b]; }); + + /* collect top_k unseen, within budget, above threshold */ + std::vector result_idx; + std::vector result_scores; + + for (int idx : order) { + if ((int)result_idx.size() >= top_k) break; + if (session.is_seen(idx)) continue; + if (scores[idx] < session.score_threshold) break; /* sorted, so stop */ + if (!session.budget_ok(db.chunks[idx].text)) break; + + result_idx.push_back(idx); + result_scores.push_back(scores[idx]); + session.mark(idx); + session.charge(db.chunks[idx].text); + } + + return rag_build_context(db, result_idx, result_scores); +} + +/* ── stateless retrieve ────────────────────────────────────── */ +std::string rag_retrieve(const RagDB &db, + llama_context *embed_ctx, + llama_model *embed_model, + const std::string &query, + int top_k) { + if (db.empty()) return {}; + + std::vector qvec; + if (!rag_embed_query(embed_ctx, embed_model, query, qvec, db.embed_dim)) + return {}; + + std::vector order(db.size()); + std::iota(order.begin(), order.end(), 0); + std::vector scores(db.size()); + for (int i = 0; i < db.size(); i++) + scores[i] = rag_cosine(qvec, db.chunks[i].embedding); + + std::sort(order.begin(), order.end(), + [&](int a, int b){ return scores[a] > scores[b]; }); + + std::vector result_idx; + std::vector result_scores; + for (int i = 0; i < std::min(top_k, db.size()); i++) { + result_idx.push_back(order[i]); + result_scores.push_back(scores[order[i]]); + } + + return rag_build_context(db, result_idx, result_scores); +} + +#endif /* RAG_IMPLEMENTATION */ diff --git a/llama/rag_index.cpp b/llama/rag_index.cpp new file mode 100644 index 0000000..8365cb2 --- /dev/null +++ b/llama/rag_index.cpp @@ -0,0 +1,200 @@ +/* + * rag_index.cpp + * + * Reads chunks.jsonl produced by chunk_headers, embeds each chunk + * using a GGUF embedding model via llama.h, saves a binary .db file. + * + * No fixed limits on chunk count or chunk length. + * + * Build: + * c++ -std=c++17 -o rag_index rag_index.cpp -lllama -lm + * + * Usage: + * ./rag_index \ + * --model nomic-embed-text-v1.5.Q4_K_M.gguf \ + * --input chunks.jsonl \ + * --output notcurses.db + */ + +#include "llama-sb.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* ── tunables ──────────────────────────────────────────────── */ +static constexpr int BATCH_SIZE = 512; +/* ─────────────────────────────────────────────────────────── */ + +/* ── on-disk chunk (variable-length text) ──────────────────── */ +/* + * db header (16 bytes): + * uint32 magic = 0x52414744 "RAGD" + * uint32 version = 2 + * uint32 n_chunks + * uint32 embed_dim + * + * per chunk: + * uint32 text_len + * char[] text (text_len bytes, no null) + * uint16 source_len + * char[] source (source_len bytes, no null) + * uint8 type_len + * char[] type (type_len bytes, no null) + * float[] embedding (embed_dim floats) + */ + +struct Chunk { + std::string text; + std::string source; + std::string type; + std::vector embedding; +}; + +/* ── tiny JSON string extractor ────────────────────────────── */ +static bool json_get_string(const std::string &json, + const std::string &key, + std::string &out) { + std::string search = "\"" + key + "\":"; + size_t pos = json.find(search); + if (pos == std::string::npos) return false; + pos += search.size(); + while (pos < json.size() && json[pos] == ' ') ++pos; + if (pos >= json.size() || json[pos] != '"') return false; + ++pos; /* skip opening quote */ + out.clear(); + while (pos < json.size()) { + char c = json[pos++]; + if (c == '\\' && pos < json.size()) { + char e = json[pos++]; + switch (e) { + case 'n': out += '\n'; break; + case 't': out += '\t'; break; + case '"': out += '"'; break; + case '\\': out += '\\'; break; + default: out += e; break; + } + } else if (c == '"') { + break; + } else { + out += c; + } + } + return true; +} + +/* ── db save ───────────────────────────────────────────────── */ +static bool save_db(const std::string &path, + const std::vector &chunks, + int embed_dim) { + std::ofstream f(path, std::ios::binary); + if (!f) { std::cerr << "cannot open for write: " << path << "\n"; return false; } + + auto write32 = [&](uint32_t v) { f.write((char*)&v, 4); }; + auto write16 = [&](uint16_t v) { f.write((char*)&v, 2); }; + auto write8 = [&](uint8_t v) { f.write((char*)&v, 1); }; + auto writestr = [&](const std::string &s, size_t max_len) { + size_t len = std::min(s.size(), max_len); + f.write(s.c_str(), (std::streamsize)len); + }; + + write32(0x52414744); /* magic "RAGD" */ + write32(2); /* version */ + write32((uint32_t)chunks.size()); /* n_chunks */ + write32((uint32_t)embed_dim); /* embed_dim */ + + for (const Chunk &c : chunks) { + write32((uint32_t)c.text.size()); + f.write(c.text.c_str(), (std::streamsize)c.text.size()); + + uint16_t src_len = (uint16_t)std::min(c.source.size(), (size_t)65535); + write16(src_len); + writestr(c.source, src_len); + + uint8_t type_len = (uint8_t)std::min(c.type.size(), (size_t)255); + write8(type_len); + writestr(c.type, type_len); + + f.write((char*)c.embedding.data(), + (std::streamsize)(embed_dim * sizeof(float))); + } + + return f.good(); +} + +/* ── main ──────────────────────────────────────────────────── */ +int main(int argc, char **argv) { + std::string model_path; + std::string input_path; + std::string output_path = "corpus.db"; + + for (int i = 1; i < argc; i++) { + if (!strcmp(argv[i], "--model") && i+1 < argc) model_path = argv[++i]; + if (!strcmp(argv[i], "--input") && i+1 < argc) input_path = argv[++i]; + if (!strcmp(argv[i], "--output") && i+1 < argc) output_path = argv[++i]; + } + + if (model_path.empty() || input_path.empty()) { + std::cerr << "usage: rag_index --model " + "--input [--output ]\n"; + return 1; + } + + /* ── load embedding model ─────────────────────────────── */ + + Llama llama; + if (!llama.load_embedding_model(model_path)) { + return 1; + } + + int embed_dim = llama.get_embed_dim(); + std::cerr << "embedding dim: " << embed_dim << "\n"; + + /* ── read and embed chunks ────────────────────────────── */ + std::vector chunks; + std::ifstream fin(input_path); + if (!fin) { std::cerr << "cannot open: " << input_path << "\n"; return 1; } + + std::string line; + int skipped = 0; + + while (std::getline(fin, line)) { + if (line.empty() || line[0] != '{') continue; + + Chunk c; + if (!json_get_string(line, "text", c.text) || + !json_get_string(line, "source", c.source)) { + ++skipped; + continue; + } + json_get_string(line, "type", c.type); + + std::cerr << "\r[" << chunks.size() << "] embedding: " + << c.text.substr(0, 40) << "..."; + + if (!llama.embed_text(c.text, c.embedding, embed_dim)) { + ++skipped; + continue; + } + + chunks.push_back(std::move(c)); + } + std::cerr << "\n"; + std::cerr << "embedded " << chunks.size() + << " chunks (" << skipped << " skipped)\n"; + + /* ── save ─────────────────────────────────────────────── */ + if (!save_db(output_path, chunks, embed_dim)) { + std::cerr << "failed to save db\n"; + return 1; + } + std::cerr << "saved β†’ " << output_path << "\n"; + + return 0; +} diff --git a/llama/samples/adventure.bas b/llama/samples/adventure.bas index eb88fea..23252f1 100644 --- a/llama/samples/adventure.bas +++ b/llama/samples/adventure.bas @@ -3,7 +3,7 @@ import llm ' Configuration const n_ctx = 5000 const n_batch = 512 -const model_path = "models/Qwen_Qwen2.5-1.5B-Instruct-GGUF-Q4/qwen2.5-1.5b-instruct-q4_k_m.gguf" +const model_path = "models/google_gemma-4-E4B-it-Q4_K_L.gguf" const max_turns = 10 ' Initialize two separate LLM instances diff --git a/llama/samples/nitro.md b/llama/samples/nitro.md index cae4896..85effcb 100644 --- a/llama/samples/nitro.md +++ b/llama/samples/nitro.md @@ -58,6 +58,7 @@ Available commands: - TOOL:DATE `[Returns the current date as string with format β€œDD/MM/YYYY”]` - TOOL:TIME `[Returns the time in β€œHH:MM:SS” format]` - TOOL:RND [Returns a random number betweem 0 and 1]` +- TOOL:RUN [invokes an external command or script in the active project]` --- ## Tool Decision Rules diff --git a/llama/samples/nitro_cli.bas b/llama/samples/nitro_cli.bas index 3450b42..28ab4c3 100644 --- a/llama/samples/nitro_cli.bas +++ b/llama/samples/nitro_cli.bas @@ -8,7 +8,7 @@ import llm ' --- Configuration --- const model = "models/Qwen3.5-9B-Q4_K_M.gguf" const knowledge_files = ["nitro.md"] -const code_files = [".py", ".cpp", ".h", ".bas", ".java", ".html", ".js", "jsp", ".tag"] +const code_files = [".py", ".c", ".cpp", ".h", ".bas", ".java", ".html", ".js", "jsp", ".tag"] ' ANSI Color Codes const RESET = chr(27) + "[0m" @@ -150,6 +150,17 @@ func tool_permission() end select end +' +' build the active project +' +func tool_run(arg1, arg2) + try + return run(join_path(sandbox_home, arg1) + " " + arg2) + catch e + return e + end try +end + ' ' Handles file system commands received from the LLM. ' @@ -191,6 +202,8 @@ func process_tool(cmd) result = iff(exist(arg1), "YES", "NO") case "TOOL:PERMISSION" result = tool_permission() + case "TOOL:RUN" + result = tool_run(arg1, arg2) case else result = "ERROR: unknown command " + op end select @@ -255,6 +268,9 @@ sub main() ' note: this construct requires recent sbasic fixes local llama = create_llama() local iter = llama.add_message("system", initialize_agent()) + local mem = llama.mem_info() + + print GREEN + " βœ… " + mem.advice + RESET sub handle_think(s) if s == "<|think|>" then @@ -321,4 +337,3 @@ endif welcome_message() main() - diff --git a/llama/test_main.cpp b/llama/test_main.cpp index fa87c43..3ab572d 100644 --- a/llama/test_main.cpp +++ b/llama/test_main.cpp @@ -56,7 +56,7 @@ int main(int argc, char ** argv) { } Llama llama; - if (llama.construct(model_path, 1024, 1024, -1, GGML_LOG_LEVEL_CONT)) { + if (llama.load_model(model_path, 1024, 1024, -1, GGML_LOG_LEVEL_CONT)) { LlamaIter iter; llama.set_max_tokens(n_predict); llama.add_message(iter, "user", prompt); From 891a6f46bcf29ffa6040e5a315cbf7a556c1140f Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Mon, 18 May 2026 21:33:57 +0930 Subject: [PATCH 108/131] LLAMA: RAG experiment to increase domain knowledge of a particular lib --- llama/CMakeLists.txt | 12 ++ llama/RAG.md | 10 +- llama/chunk_headers.cpp | 1 + llama/llama-sb-rag.cpp | 257 +++++++++++++++++++++++++++++++ llama/llama-sb.cpp | 56 +------ llama/llama-sb.h | 10 +- llama/llama.cpp | 2 +- llama/rag.hpp | 328 ---------------------------------------- llama/rag_index.cpp | 12 +- 9 files changed, 293 insertions(+), 395 deletions(-) create mode 100644 llama/llama-sb-rag.cpp delete mode 100644 llama/rag.hpp diff --git a/llama/CMakeLists.txt b/llama/CMakeLists.txt index 629a269..749b55e 100644 --- a/llama/CMakeLists.txt +++ b/llama/CMakeLists.txt @@ -113,6 +113,7 @@ add_subdirectory(${LLAMA_DIR}) set(PLUGIN_SOURCES main.cpp llama-sb.cpp + llama-sb-rag.cpp ../include/param.cpp ../include/hashmap.cpp ../include/apiexec.cpp @@ -193,6 +194,17 @@ set_target_properties(rag_index PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin ) +# ----------------------------- +# Header preparation for RAG indexer +# ----------------------------- +add_executable(chunk_headers + chunk_headers.cpp +) + +set_target_properties(chunk_headers PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin +) + # ------------------------------------------------------------------ # Android native library # ------------------------------------------------------------------ diff --git a/llama/RAG.md b/llama/RAG.md index ee5e8e7..6c69c06 100644 --- a/llama/RAG.md +++ b/llama/RAG.md @@ -21,7 +21,7 @@ notcurses headers chunk_headers ← semantic chunker, outputs chunks.jsonl β”‚ β–Ό -rag_index ← embeds each chunk via nomic-embed-text GGUF +rag_index ← embeds each chunk via qwen3-embedding-0.6b-q4_k_m.gguf β”‚ β–Ό notcurses.db ← binary vector store (embeddings + text) @@ -64,9 +64,7 @@ history ← appended for next turn (KV cache intact) - [llama.cpp](https://github.com/ggerganov/llama.cpp) β€” `libllama` + `llama.h` - A GGUF **inference model** β€” tested with `Qwen3.5-9B-Q4_K_M.gguf` -- A GGUF **embedding model** β€” - `nomic-embed-text-v1.5.Q4_K_M.gguf` - ([download](https://huggingface.co/nomic-ai/nomic-embed-text-v1.5-GGUF)) +- A GGUF **embedding model** β€” `qwen3-embedding-0.6b-q4_k_m.gguf` - C++17 compiler (gcc 8+, clang 7+, MSVC 2019+) --- @@ -114,7 +112,7 @@ head -5 chunks.jsonl | python3 -m json.tool ```bash ./rag_index \ - --model nomic-embed-text-v1.5.Q4_K_M.gguf \ + --model qwen3-embedding-0.6b-q4_k_m.gguf \ --input chunks.jsonl \ --output notcurses.db ``` @@ -127,7 +125,7 @@ until the library changes. ```bash ./example \ --model Qwen3.5-9B-Q4_K_M.gguf \ - --embed nomic-embed-text-v1.5.Q4_K_M.gguf \ + --embed qwen3-embedding-0.6b-q4_k_m.gguf \ --db notcurses.db ``` diff --git a/llama/chunk_headers.cpp b/llama/chunk_headers.cpp index 51ecd66..0d8eff0 100644 --- a/llama/chunk_headers.cpp +++ b/llama/chunk_headers.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include namespace fs = std::filesystem; diff --git a/llama/llama-sb-rag.cpp b/llama/llama-sb-rag.cpp new file mode 100644 index 0000000..77c08b3 --- /dev/null +++ b/llama/llama-sb-rag.cpp @@ -0,0 +1,257 @@ +// This file is part of SmallBASIC +// +// This program is distributed under the terms of the GPL v2.0 or later +// Download the GNU Public License (GPL) from www.gnu.org +// +// Copyright(C) 2026 Chris Warren-Smith + +#include "llama-sb.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct RagChunk { + std::string text; + std::string source; + std::string type; + std::vector embedding; +}; + +struct RagDB { + std::vector chunks; + int embed_dim = 0; + + int size() const { return (int)chunks.size(); } + bool empty() const { return chunks.empty(); } +}; + +// +// per-session deduplication + token budget +// +struct RagSession { + std::vector seen; /* sized to db.size() on init */ + int tokens_used = 0; + int tokens_max = 0; /* set to your n_ctx */ + float score_threshold = 0.60f; /* skip weak matches */ + + void init(int n_chunks, int ctx_size) { + seen.assign(n_chunks, false); + tokens_used = 0; + tokens_max = ctx_size; + } + + void reset() { + std::fill(seen.begin(), seen.end(), false); + tokens_used = 0; + } + + bool is_seen(int idx) const { return idx < (int)seen.size() && seen[idx]; } + void mark(int idx) { if (idx < (int)seen.size()) seen[idx] = true; } + + /* rough token estimate: 1 token β‰ˆ 4 chars */ + bool budget_ok(const std::string &text) const { + return tokens_max == 0 || + (tokens_used + (int)text.size() / 4) < (int)(tokens_max * 0.85f); + } + + void charge(const std::string &text) { + tokens_used += (int)text.size() / 4; + } +}; + +bool Llama::embed_text(const std::string &text, std::vector &out, int embed_dim) { + vector tokens = tokenize(text); + if (tokens.size() == 0) { + return false; + } + + // truncate to context window + int n_ctx = llama_n_ctx(_ctx); + int n = tokens.size(); + if (n > n_ctx) { + _last_error = std::format("warning: chunk truncated {} -> {} tokens ", n, n_ctx); + n = n_ctx; + tokens.resize(n); + } + + llama_memory_clear(llama_get_memory(_ctx), true); + + if (!batch_decode_tokens(tokens)) { + return false; + } + + float *emb = llama_get_embeddings_seq(_ctx, 0); + if (!emb) { + emb = llama_get_embeddings_ith(_ctx, n - 1); + } + + if (!emb) { + _last_error = "no embedding returned\n"; + return false; + } + + out.assign(emb, emb + embed_dim); + + /* L2 normalize */ + float norm = 0.0f; + for (float v : out) { + norm += v * v; + } + norm = std::sqrt(norm); + if (norm > 1e-9f) { + for (float &v : out) { + v /= norm; + } + } + + return true; +} + +bool Llama::rag_load(RagDB &db, const std::string &path) { + std::ifstream f(path, std::ios::binary); + if (!f) { + _last_error = std::format("rag_load: cannot open {}", path); + return false; + } + + auto read32 = [&]() -> uint32_t { + uint32_t v = 0; f.read((char*)&v, 4); return v; + }; + auto read16 = [&]() -> uint16_t { + uint16_t v = 0; f.read((char*)&v, 2); return v; + }; + auto read8 = [&]() -> uint8_t { + uint8_t v = 0; f.read((char*)&v, 1); return v; + }; + auto readstr = [&](size_t len) -> std::string { + std::string s(len, '\0'); + f.read(&s[0], (std::streamsize)len); + return s; + }; + + uint32_t magic = read32(); + uint32_t version = read32(); + uint32_t n = read32(); + uint32_t edim = read32(); + + if (magic != 0x52414744) { + _last_error = "rag_load: bad magic"; + return false; + } + if (version != 2) { + _last_error = std::format("rag_load: unsupported version {} (expected 2)", version); + return false; + } + + db.embed_dim = (int)edim; + db.chunks.resize(n); + + for (uint32_t i = 0; i < n; i++) { + RagChunk &c = db.chunks[i]; + + uint32_t text_len = read32(); + c.text = readstr(text_len); + + uint16_t src_len = read16(); + c.source = readstr(src_len); + + uint8_t type_len = read8(); + c.type = readstr(type_len); + + c.embedding.resize(edim); + f.read((char*)c.embedding.data(), (std::streamsize)(edim * sizeof(float))); + } + + if (!f) { + _last_error = "rag_load: read error"; + return false; + } + + std::cerr << "rag: loaded " << db.chunks.size() + << " chunks (dim=" << db.embed_dim + << ") from " << path << "\n"; + return true; +} + +// +// cosine similarity (vectors already L2-normalized) +// +static float rag_cosine(const std::vector &a, + const std::vector &b) { + float dot = 0.0f; + size_t n = std::min(a.size(), b.size()); + for (size_t i = 0; i < n; i++) { + dot += a[i] * b[i]; + } + return dot; +} + +// +// build context string from ranked results +// +static std::string rag_build_context(const RagDB &db, + const std::vector &indices, + const std::vector &scores) { + std::ostringstream out; + for (size_t i = 0; i < indices.size(); i++) { + const RagChunk &c = db.chunks[indices[i]]; + out << "// source: " << c.source + << " [" << c.type << "]" + << " (score: " << scores[i] << ")\n" + << c.text << "\n---\n"; + } + return out.str(); +} + +// +// retrieve with session +// +std::string Llama::rag_retrieve(const RagDB &db, + const std::string &query, + int top_k, + RagSession &session) { + if (db.empty()) { + return {}; + } + + std::vector qvec; + std::string text = "Instruct: Given a programming question, retrieve relevant API documentation\nQuery: " + query; + if (!embed_text(text, qvec, db.embed_dim)) { + return {}; + } + + // score all chunks + std::vector order(db.size()); + std::iota(order.begin(), order.end(), 0); + std::vector scores(db.size()); + for (int i = 0; i < db.size(); i++) + scores[i] = rag_cosine(qvec, db.chunks[i].embedding); + + std::sort(order.begin(), order.end(), + [&](int a, int b){ return scores[a] > scores[b]; }); + + // collect top_k unseen, within budget, above threshold + std::vector result_idx; + std::vector result_scores; + + for (int idx : order) { + if ((int)result_idx.size() >= top_k) break; + if (session.is_seen(idx)) continue; + if (scores[idx] < session.score_threshold) break; /* sorted, so stop */ + if (!session.budget_ok(db.chunks[idx].text)) break; + + result_idx.push_back(idx); + result_scores.push_back(scores[idx]); + session.mark(idx); + session.charge(db.chunks[idx].text); + } + + return rag_build_context(db, result_idx, result_scores); +} diff --git a/llama/llama-sb.cpp b/llama/llama-sb.cpp index 05463fb..9e3b54a 100644 --- a/llama/llama-sb.cpp +++ b/llama/llama-sb.cpp @@ -203,66 +203,14 @@ bool Llama::load_embedding_model(string model_path) { _ctx = llama_init_from_model(_model, cparams); if (!_ctx) { set_last_error("Create context"); + } else { + _vocab = llama_model_get_vocab(_model); } } return _last_error.empty(); } -int Llama::get_embed_dim() { - return _model != nullptr ? llama_model_n_embd(_model) : 0; -} - -bool Llama::embed_text(const std::string &text, std::vector &out, int embed_dim) { - std::string prefixed = "search_document: " + text; - - vector tokens = tokenize(prefixed); - if (tokens.size() == 0) { - return false; - } - - // truncate to context window - int n_ctx = llama_n_ctx(_ctx); - int n = tokens.size(); - if (n > n_ctx) { - _last_error = std::format("warning: chunk truncated {} -> {} tokens ", n, n_ctx); - n = n_ctx; - tokens.resize(n); - } - - llama_memory_clear(llama_get_memory(_ctx), true); - - if (!batch_decode_tokens(tokens)) { - return false; - } - - float *emb = llama_get_embeddings_seq(_ctx, 0); - if (!emb) { - emb = llama_get_embeddings_ith(_ctx, n - 1); - } - - if (!emb) { - _last_error = "no embedding returned\n"; - return false; - } - - out.assign(emb, emb + embed_dim); - - /* L2 normalize */ - float norm = 0.0f; - for (float v : out) { - norm += v * v; - } - norm = std::sqrt(norm); - if (norm > 1e-9f) { - for (float &v : out) { - v /= norm; - } - } - - return true; -} - void Llama::set_grammar(const string &src, const string &root) { _grammar_src = src; _grammar_root = root; diff --git a/llama/llama-sb.h b/llama/llama-sb.h index 99a3864..0ca3998 100644 --- a/llama/llama-sb.h +++ b/llama/llama-sb.h @@ -14,6 +14,10 @@ using namespace std; +struct Llama; +struct RagDB; +struct RagSession; + struct LlamaMemoryInfo { // KV cache int kv_used; // slots currently used @@ -34,8 +38,6 @@ struct LlamaMemoryInfo { string advice; }; -struct Llama; - struct LlamaIter { explicit LlamaIter(); ~LlamaIter() {} @@ -101,7 +103,9 @@ struct Llama { // rag support bool embed_text(const std::string &text, std::vector &out, int embed); - int get_embed_dim(); + int get_embed_dim() const { return _model != nullptr ? llama_model_n_embd(_model) : 0; } + bool rag_load(RagDB &db, const std::string &path); + std::string rag_retrieve(const RagDB &db, const std::string &query, int top_k, RagSession &session); private: bool batch_decode_tokens(vector &tokens); diff --git a/llama/llama.cpp b/llama/llama.cpp index 7f3f843..3fbadb0 160000 --- a/llama/llama.cpp +++ b/llama/llama.cpp @@ -1 +1 @@ -Subproject commit 7f3f843c31cd32dc4adc10b393342dfee071c332 +Subproject commit 3fbadb06dc867d3236937705477e090724ebbc6e diff --git a/llama/rag.hpp b/llama/rag.hpp deleted file mode 100644 index 3e133da..0000000 --- a/llama/rag.hpp +++ /dev/null @@ -1,328 +0,0 @@ -/* - * rag.hpp - * - * Single-header C++ RAG runtime library. - * No fixed limits on chunk count, chunk length, or embedding dimension. - * Includes RagSession for deduplication across turns. - * - * Usage (in ONE .cpp file): - * #define RAG_IMPLEMENTATION - * #include "rag.hpp" - * - * All other files: - * #include "rag.hpp" - * - * Db format version: 2 (written by rag_index.cpp) - */ - -#pragma once - -#include "llama.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -/* ═══════════════════════════════════════════════════════════ - * Data structures - * ═══════════════════════════════════════════════════════════ */ - -struct RagChunk { - std::string text; - std::string source; - std::string type; - std::vector embedding; -}; - -struct RagDB { - std::vector chunks; - int embed_dim = 0; - - int size() const { return (int)chunks.size(); } - bool empty() const { return chunks.empty(); } -}; - -/* ── per-session deduplication + token budget ──────────────── */ -struct RagSession { - std::vector seen; /* sized to db.size() on init */ - int tokens_used = 0; - int tokens_max = 0; /* set to your n_ctx */ - float score_threshold = 0.60f; /* skip weak matches */ - - void init(int n_chunks, int ctx_size) { - seen.assign(n_chunks, false); - tokens_used = 0; - tokens_max = ctx_size; - } - - void reset() { - std::fill(seen.begin(), seen.end(), false); - tokens_used = 0; - } - - bool is_seen(int idx) const { return idx < (int)seen.size() && seen[idx]; } - void mark(int idx) { if (idx < (int)seen.size()) seen[idx] = true; } - - /* rough token estimate: 1 token β‰ˆ 4 chars */ - bool budget_ok(const std::string &text) const { - return tokens_max == 0 || - (tokens_used + (int)text.size() / 4) < (int)(tokens_max * 0.85f); - } - - void charge(const std::string &text) { - tokens_used += (int)text.size() / 4; - } -}; - -/* ═══════════════════════════════════════════════════════════ - * API declarations - * ═══════════════════════════════════════════════════════════ */ - -/* Load .db file produced by rag_index.cpp (version 2). - * Returns true on success. */ -bool rag_load(RagDB &db, const std::string &path); - -/* - * Embed query, score all chunks, inject top_k unseen results into out. - * Skips chunks already in session.seen and below session.score_threshold. - * Updates session.seen and session.tokens_used. - * Returns the context string (empty if nothing retrieved). - */ -std::string rag_retrieve(const RagDB &db, - llama_context *embed_ctx, - llama_model *embed_model, - const std::string &query, - int top_k, - RagSession &session); - -/* - * Stateless overload β€” no deduplication, no budget tracking. - * Useful for one-shot queries or testing retrieval quality. - */ -std::string rag_retrieve(const RagDB &db, - llama_context *embed_ctx, - llama_model *embed_model, - const std::string &query, - int top_k); - -/* ═══════════════════════════════════════════════════════════ - * Implementation - * ═══════════════════════════════════════════════════════════ */ -#ifdef RAG_IMPLEMENTATION - -/* ── db load ───────────────────────────────────────────────── */ -bool rag_load(RagDB &db, const std::string &path) { - std::ifstream f(path, std::ios::binary); - if (!f) { std::cerr << "rag_load: cannot open " << path << "\n"; return false; } - - auto read32 = [&]() -> uint32_t { - uint32_t v = 0; f.read((char*)&v, 4); return v; - }; - auto read16 = [&]() -> uint16_t { - uint16_t v = 0; f.read((char*)&v, 2); return v; - }; - auto read8 = [&]() -> uint8_t { - uint8_t v = 0; f.read((char*)&v, 1); return v; - }; - auto readstr = [&](size_t len) -> std::string { - std::string s(len, '\0'); - f.read(&s[0], (std::streamsize)len); - return s; - }; - - uint32_t magic = read32(); - uint32_t version = read32(); - uint32_t n = read32(); - uint32_t edim = read32(); - - if (magic != 0x52414744) { - std::cerr << "rag_load: bad magic\n"; return false; - } - if (version != 2) { - std::cerr << "rag_load: unsupported version " << version - << " (expected 2)\n"; return false; - } - - db.embed_dim = (int)edim; - db.chunks.resize(n); - - for (uint32_t i = 0; i < n; i++) { - RagChunk &c = db.chunks[i]; - - uint32_t text_len = read32(); - c.text = readstr(text_len); - - uint16_t src_len = read16(); - c.source = readstr(src_len); - - uint8_t type_len = read8(); - c.type = readstr(type_len); - - c.embedding.resize(edim); - f.read((char*)c.embedding.data(), - (std::streamsize)(edim * sizeof(float))); - } - - if (!f) { std::cerr << "rag_load: read error\n"; return false; } - - std::cerr << "rag: loaded " << db.chunks.size() - << " chunks (dim=" << db.embed_dim - << ") from " << path << "\n"; - return true; -} - -/* ── embed query ───────────────────────────────────────────── */ -static bool rag_embed_query(llama_context *ctx, - llama_model *model, - const std::string &query, - std::vector &out, - int embed_dim) { - std::string prefixed = "search_query: " + query; - - int n = -llama_tokenize(model, - prefixed.c_str(), (int)prefixed.size(), - nullptr, 0, true, true); - if (n <= 0) return false; - - std::vector tokens(n); - llama_tokenize(model, - prefixed.c_str(), (int)prefixed.size(), - tokens.data(), n, true, true); - - int n_ctx = llama_n_ctx(ctx); - if (n > n_ctx) n = n_ctx; - - llama_kv_cache_clear(ctx); - - llama_batch batch = llama_batch_init(n, 0, 1); - for (int i = 0; i < n; i++) { - llama_seq_id seq = 0; - llama_batch_add(batch, tokens[i], i, &seq, 1, i == n - 1); - } - - if (llama_decode(ctx, batch) != 0) { - llama_batch_free(batch); - return false; - } - - float *emb = llama_get_embeddings_seq(ctx, 0); - if (!emb) emb = llama_get_embeddings_ith(ctx, n - 1); - if (!emb) { llama_batch_free(batch); return false; } - - out.assign(emb, emb + embed_dim); - - float norm = 0.0f; - for (float v : out) norm += v * v; - norm = std::sqrt(norm); - if (norm > 1e-9f) - for (float &v : out) v /= norm; - - llama_batch_free(batch); - return true; -} - -/* ── cosine similarity (vectors already L2-normalized) ─────── */ -static float rag_cosine(const std::vector &a, - const std::vector &b) { - float dot = 0.0f; - size_t n = std::min(a.size(), b.size()); - for (size_t i = 0; i < n; i++) dot += a[i] * b[i]; - return dot; -} - -/* ── build context string from ranked results ──────────────── */ -static std::string rag_build_context( - const RagDB &db, - const std::vector &indices, - const std::vector &scores) { - std::ostringstream out; - for (size_t i = 0; i < indices.size(); i++) { - const RagChunk &c = db.chunks[indices[i]]; - out << "// source: " << c.source - << " [" << c.type << "]" - << " (score: " << scores[i] << ")\n" - << c.text << "\n---\n"; - } - return out.str(); -} - -/* ── retrieve with session ─────────────────────────────────── */ -std::string rag_retrieve(const RagDB &db, - llama_context *embed_ctx, - llama_model *embed_model, - const std::string &query, - int top_k, - RagSession &session) { - if (db.empty()) return {}; - - std::vector qvec; - if (!rag_embed_query(embed_ctx, embed_model, query, qvec, db.embed_dim)) - return {}; - - /* score all chunks */ - std::vector order(db.size()); - std::iota(order.begin(), order.end(), 0); - std::vector scores(db.size()); - for (int i = 0; i < db.size(); i++) - scores[i] = rag_cosine(qvec, db.chunks[i].embedding); - - std::sort(order.begin(), order.end(), - [&](int a, int b){ return scores[a] > scores[b]; }); - - /* collect top_k unseen, within budget, above threshold */ - std::vector result_idx; - std::vector result_scores; - - for (int idx : order) { - if ((int)result_idx.size() >= top_k) break; - if (session.is_seen(idx)) continue; - if (scores[idx] < session.score_threshold) break; /* sorted, so stop */ - if (!session.budget_ok(db.chunks[idx].text)) break; - - result_idx.push_back(idx); - result_scores.push_back(scores[idx]); - session.mark(idx); - session.charge(db.chunks[idx].text); - } - - return rag_build_context(db, result_idx, result_scores); -} - -/* ── stateless retrieve ────────────────────────────────────── */ -std::string rag_retrieve(const RagDB &db, - llama_context *embed_ctx, - llama_model *embed_model, - const std::string &query, - int top_k) { - if (db.empty()) return {}; - - std::vector qvec; - if (!rag_embed_query(embed_ctx, embed_model, query, qvec, db.embed_dim)) - return {}; - - std::vector order(db.size()); - std::iota(order.begin(), order.end(), 0); - std::vector scores(db.size()); - for (int i = 0; i < db.size(); i++) - scores[i] = rag_cosine(qvec, db.chunks[i].embedding); - - std::sort(order.begin(), order.end(), - [&](int a, int b){ return scores[a] > scores[b]; }); - - std::vector result_idx; - std::vector result_scores; - for (int i = 0; i < std::min(top_k, db.size()); i++) { - result_idx.push_back(order[i]); - result_scores.push_back(scores[order[i]]); - } - - return rag_build_context(db, result_idx, result_scores); -} - -#endif /* RAG_IMPLEMENTATION */ diff --git a/llama/rag_index.cpp b/llama/rag_index.cpp index 8365cb2..9f8a4fe 100644 --- a/llama/rag_index.cpp +++ b/llama/rag_index.cpp @@ -159,13 +159,18 @@ int main(int argc, char **argv) { /* ── read and embed chunks ────────────────────────────── */ std::vector chunks; std::ifstream fin(input_path); - if (!fin) { std::cerr << "cannot open: " << input_path << "\n"; return 1; } + if (!fin) { + std::cerr << "cannot open: " << input_path << "\n"; + return 1; + } std::string line; int skipped = 0; while (std::getline(fin, line)) { - if (line.empty() || line[0] != '{') continue; + if (line.empty() || line[0] != '{') { + continue; + } Chunk c; if (!json_get_string(line, "text", c.text) || @@ -178,7 +183,8 @@ int main(int argc, char **argv) { std::cerr << "\r[" << chunks.size() << "] embedding: " << c.text.substr(0, 40) << "..."; - if (!llama.embed_text(c.text, c.embedding, embed_dim)) { + std::string text = "Instruct: Represent this API documentation for code retrieval\nQuery: " + c.text; + if (!llama.embed_text(text, c.embedding, embed_dim)) { ++skipped; continue; } From 702026e20a4de90d26288c99ec1c2ea2d482ccfe Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Wed, 20 May 2026 19:55:59 +0930 Subject: [PATCH 109/131] simple agent using llama-sb.h --- llama/nitro.cpp | 1212 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1212 insertions(+) create mode 100644 llama/nitro.cpp diff --git a/llama/nitro.cpp b/llama/nitro.cpp new file mode 100644 index 0000000..6057ff2 --- /dev/null +++ b/llama/nitro.cpp @@ -0,0 +1,1212 @@ +// nitro.cpp β€” Nitro Agent +// A standalone agentic LLM shell with notcurses TUI. +// Uses llama-sb.h as the sole llama.cpp integration layer. +// +// Build (example): +// g++ -std=c++20 -O2 nitro.cpp llama-sb.cpp \ +// -I/path/to/llama.cpp/include \ +// -L/path/to/llama.cpp/build/src \ +// -lllama -lggml -lnotcurses-core -lnotcurses \ +// -o nitro +// +// Usage: +// ./nitro [options] [project_dir] +// +// Options: +// -m, --model GGUF model to load on startup +// -e, --embed embedding model for RAG +// -g, --gpu-layers layers to offload to GPU (default: 32) +// +// Slash commands: +// /model β€” load / hot-reload a GGUF model +// /embed β€” load an embedding model for RAG +// /rag β€” index a file or directory into RAG +// /memory β€” show KV / VRAM / layer stats +// /clear β€” reset conversation (keeps system prompt) +// /help β€” list commands +// +// Tool protocol (LLM emits, Nitro executes): +// TOOL:LIST [dir] +// TOOL:READ +// TOOL:WRITE +// TOOL:EXISTS +// TOOL:RUN [args] +// TOOL:DATE +// TOOL:TIME +// TOOL:RND +// TOOL:PERMISSION +// +// Copyright (C) 2026 Chris Warren-Smith β€” GPLv2 or later + +// ─── Standard library ──────────────────────────────────────────────────────── +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ─── Integration layer (sole llama.cpp dependency for nitro) ───────────────── +#include "llama-sb.h" +#include "llama-sb-rag.h" + +// ─── TUI ───────────────────────────────────────────────────────────────────── +#include + +namespace fs = std::filesystem; + +// ═══════════════════════════════════════════════════════════════════════════ +// Forward declarations +// ═══════════════════════════════════════════════════════════════════════════ +struct NitroConfig; +struct TuiState; +struct AgentState; + +static std::string join_path(const std::string &a, const std::string &b); +static std::string read_file(const std::string &path); +static bool write_file(const std::string &path, const std::string &data); +static std::string list_dir(const std::string &path); +static bool path_in_sandbox(const std::string &sandbox, const std::string &path); +static std::string strip_code_fences(const std::string &filename, const std::string &src); +static std::string process_tool(const std::string &line, const std::string &sandbox, + TuiState &tui); +static std::string build_system_prompt(const std::vector &knowledge_files, + const std::string &sandbox); + +// ═══════════════════════════════════════════════════════════════════════════ +// Config (mirrors the SB agent constants) +// ═══════════════════════════════════════════════════════════════════════════ + +struct NitroConfig { + std::string model_path; // empty = no model yet; set via -m/--model or /model + std::string embed_path; + std::string sandbox; + int n_ctx = 65536; + int n_batch = 512; + int n_gpu_layers = 32; + int n_max_tokens = 4096; + int log_level = GGML_LOG_LEVEL_CONT; + float temperature = 0.6f; + float top_p = 0.95f; + float min_p = 0.0f; + int top_k = 20; + float penalty_repeat = 1.0f; + int penalty_last_n = 256; + std::vector knowledge_files; + int rag_top_k = 5; +}; + +// ═══════════════════════════════════════════════════════════════════════════ +// Notcurses TUI +// ═══════════════════════════════════════════════════════════════════════════ +// +// β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ header (1 row) ─────────────────────────────────┐ +// β”‚ ✦ NITRO model: … tok/s: … KV: …% VRAM: …% β”‚ +// β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +// β”‚ β”‚ +// β”‚ chat pane (rows 1 … term_rows-3) β”‚ +// β”‚ β”‚ +// β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +// β”‚ ───────────────────────────────────── (separator) β”‚ +// β”‚ ❯ input β”‚ +// β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +struct TuiState { + // ── notcurses handles ────────────────────────────────────────────── + struct notcurses *nc = nullptr; + struct ncplane *stdpl = nullptr; + struct ncplane *header = nullptr; + struct ncplane *chatpl = nullptr; + struct ncplane *inputpl = nullptr; + + // ── chat buffer ─────────────────────────────────────────────────── + std::vector chat_lines; + int scroll_offset = 0; // lines scrolled up from bottom (0 = pinned) + std::mutex lines_mutex; + + // ── streaming accumulator ───────────────────────────────────────── + // Tokens arrive without newlines; we accumulate here and flush on \n. + std::string token_acc; + + // ── input ───────────────────────────────────────────────────────── + std::string input_buf; + size_t cursor_pos = 0; + + // ── status bar values (written by agent loop) ───────────────────── + std::string current_model = "none"; + float tokens_per_sec = 0.0f; + int kv_used = 0; + int kv_total = 1; + size_t vram_used = 0; + size_t vram_total = 1; + + int term_rows = 0; + int term_cols = 0; + + // ── lifecycle ───────────────────────────────────────────────────── + void init(); + void destroy(); + void resize(); + + // ── draw ────────────────────────────────────────────────────────── + void redraw_header(); + void redraw_chat(); + void redraw_input(); + void redraw_all(); + + // ── content helpers ─────────────────────────────────────────────── + // Append a complete line (wraps at terminal width, colour-coded by prefix). + void append_line(const std::string &line); + // Feed a streaming token fragment; flushes complete lines on \n. + void append_token(const std::string &token); + // Flush whatever is left in token_acc as a final line. + void flush_token_acc(); + + // ── interaction ─────────────────────────────────────────────────── + // Show a YES/NO confirm dialog in the input plane; writes "YES" or "NO". + void confirm_dialog(const std::string &prompt, std::string &result); + // Blocking readline with cursor, arrow-key scrolling, basic editing. + std::string readline_blocking(); +}; + +// ─── colour helpers ────────────────────────────────────────────────────── + +static inline uint64_t fg_rgb(uint32_t r, uint32_t g, uint32_t b) { + return NCCHANNELS_INITIALIZER(r, g, b, 0, 0, 0); +} + +// ─── TuiState::init ────────────────────────────────────────────────────── + +void TuiState::init() { + notcurses_options opts{}; + opts.flags = NCOPTION_SUPPRESS_BANNERS; + nc = notcurses_init(&opts, nullptr); + if (!nc) { std::fputs("notcurses_init failed\n", stderr); std::exit(1); } + + stdpl = notcurses_stdplane(nc); + notcurses_term_dim_yx(nc, (unsigned *)&term_rows, (unsigned *)&term_cols); + + // Header: row 0 + ncplane_options hopt{}; + hopt.y = 0; hopt.x = 0; + hopt.rows = 1; hopt.cols = (unsigned)term_cols; + header = ncplane_create(stdpl, &hopt); + + // Chat pane: rows 1 … term_rows-3 + int chat_rows = std::max(1, term_rows - 3); + ncplane_options copt{}; + copt.y = 1; copt.x = 0; + copt.rows = (unsigned)chat_rows; copt.cols = (unsigned)term_cols; + chatpl = ncplane_create(stdpl, &copt); + + // Input pane: last 2 rows + ncplane_options iopt{}; + iopt.y = term_rows - 2; iopt.x = 0; + iopt.rows = 2; iopt.cols = (unsigned)term_cols; + inputpl = ncplane_create(stdpl, &iopt); + + redraw_all(); +} + +void TuiState::destroy() { + if (nc) { notcurses_stop(nc); nc = nullptr; } +} + +void TuiState::resize() { + notcurses_term_dim_yx(nc, (unsigned *)&term_rows, (unsigned *)&term_cols); + ncplane_resize_simple(header, 1, (unsigned)term_cols); + int cr = std::max(1, term_rows - 3); + ncplane_resize_simple(chatpl, (unsigned)cr, (unsigned)term_cols); + ncplane_move_yx(inputpl, term_rows - 2, 0); + ncplane_resize_simple(inputpl, 2, (unsigned)term_cols); + redraw_all(); +} + +// ─── TuiState::redraw_* ────────────────────────────────────────────────── + +void TuiState::redraw_header() { + ncplane_erase(header); + ncplane_set_base(header, " ", 0, fg_rgb(30, 40, 55)); + + float kv_pct = kv_total > 0 ? 100.f * (float)kv_used / (float)kv_total : 0.f; + float vram_pct = vram_total > 0 ? 100.f * (float)vram_used / (float)vram_total : 0.f; + + char buf[512]; + int n = std::snprintf(buf, sizeof(buf), + " ✦ NITRO β”‚ %-32s β”‚ %5.1f tok/s β”‚ KV %4.1f%% VRAM %4.1f%%", + current_model.c_str(), (double)tokens_per_sec, + (double)kv_pct, (double)vram_pct); + if (n > term_cols) buf[term_cols] = '\0'; + + ncplane_set_channels(header, fg_rgb(130, 220, 200)); + ncplane_putstr_yx(header, 0, 0, buf); +} + +void TuiState::redraw_chat() { + ncplane_erase(chatpl); + unsigned rows, cols; + ncplane_dim_yx(chatpl, &rows, &cols); + + std::lock_guard lk(lines_mutex); + int total = (int)chat_lines.size(); + int visible = (int)rows; + int start = std::max(0, total - visible - scroll_offset); + int end = std::min(total, start + visible); + + for (int i = start, row = 0; i < end; ++i, ++row) { + const std::string &line = chat_lines[i]; + + uint64_t ch; + if (line.rfind("You: ", 0) == 0) ch = fg_rgb(100, 200, 255); + else if (line.rfind("Nitro: ", 0) == 0) ch = fg_rgb(180, 255, 180); + else if (line.rfind("[tool]", 0) == 0) ch = fg_rgb(255, 180, 80); + else if (line.rfind("[err]", 0) == 0) ch = fg_rgb(255, 80, 80); + else if (line.rfind("[sys]", 0) == 0) ch = fg_rgb(140, 140, 200); + else ch = fg_rgb(210, 210, 210); + + ncplane_set_channels(chatpl, ch); + std::string display = line.size() > cols ? line.substr(0, cols) : line; + ncplane_putstr_yx(chatpl, row, 0, display.c_str()); + } +} + +void TuiState::redraw_input() { + ncplane_erase(inputpl); + + // Separator + ncplane_set_channels(inputpl, fg_rgb(80, 120, 160)); + std::string sep(term_cols, '-'); + ncplane_putstr_yx(inputpl, 0, 0, sep.c_str()); + + // Prompt + buffer + const std::string prompt = " ❯ "; + ncplane_set_channels(inputpl, fg_rgb(230, 230, 230)); + ncplane_putstr_yx(inputpl, 1, 0, prompt.c_str()); + + int max_w = std::max(0, term_cols - (int)prompt.size() - 1); + std::string display = input_buf; + if ((int)display.size() > max_w && max_w > 0) + display = display.substr(display.size() - max_w); + ncplane_putstr_yx(inputpl, 1, (int)prompt.size(), display.c_str()); + + // Cursor position + int cx = std::min((int)prompt.size() + (int)cursor_pos, term_cols - 1); + ncplane_cursor_move_yx(inputpl, 1, cx); +} + +void TuiState::redraw_all() { + redraw_header(); + redraw_chat(); + redraw_input(); + notcurses_render(nc); +} + +// ─── TuiState content helpers ───────────────────────────────────────────── + +void TuiState::append_line(const std::string &line) { + std::lock_guard lk(lines_mutex); + int w = std::max(1, term_cols - 1); + if ((int)line.size() <= w) { + chat_lines.push_back(line); + } else { + for (int off = 0; off < (int)line.size(); off += w) + chat_lines.push_back(line.substr(off, w)); + } +} + +void TuiState::append_token(const std::string &token) { + token_acc += token; + for (;;) { + auto pos = token_acc.find('\n'); + if (pos == std::string::npos) break; + append_line(token_acc.substr(0, pos)); + token_acc = token_acc.substr(pos + 1); + } + redraw_chat(); + notcurses_render(nc); +} + +void TuiState::flush_token_acc() { + if (!token_acc.empty()) { + append_line(token_acc); + token_acc.clear(); + redraw_chat(); + notcurses_render(nc); + } +} + +// ─── TuiState::confirm_dialog ───────────────────────────────────────────── + +void TuiState::confirm_dialog(const std::string &prompt, std::string &result) { + ncplane_erase(inputpl); + ncplane_set_channels(inputpl, fg_rgb(255, 200, 80)); + std::string msg = " " + prompt + " [y/n] ❯ "; + ncplane_putstr_yx(inputpl, 1, 0, msg.c_str()); + notcurses_render(nc); + + std::string answer; + for (;;) { + ncinput ni{}; + notcurses_get_blocking(nc, &ni); + if (ni.id == NCKEY_ENTER || ni.id == '\r' || ni.id == '\n') break; + if (ni.id == NCKEY_BACKSPACE && !answer.empty()) { answer.pop_back(); } + else if (ni.id >= 32 && ni.id < 127) { answer += (char)ni.id; } + + ncplane_erase(inputpl); + ncplane_set_channels(inputpl, fg_rgb(255, 200, 80)); + ncplane_putstr_yx(inputpl, 1, 0, (msg + answer).c_str()); + notcurses_render(nc); + } + + std::string lo = answer; + std::transform(lo.begin(), lo.end(), lo.begin(), ::tolower); + result = (lo == "y" || lo == "yes" || lo == "sure" || lo == "k") ? "YES" : "NO"; + redraw_input(); + notcurses_render(nc); +} + +// ─── TuiState::readline_blocking ────────────────────────────────────────── + +std::string TuiState::readline_blocking() { + input_buf.clear(); + cursor_pos = 0; + redraw_input(); + notcurses_render(nc); + + for (;;) { + ncinput ni{}; + notcurses_get_blocking(nc, &ni); + + if (ni.id == NCKEY_ENTER || ni.id == '\r' || ni.id == '\n') { + std::string result = input_buf; + input_buf.clear(); cursor_pos = 0; + redraw_input(); notcurses_render(nc); + return result; + } + if (ni.id == NCKEY_BACKSPACE || ni.id == 127) { + if (cursor_pos > 0) { input_buf.erase(cursor_pos - 1, 1); --cursor_pos; } + } else if (ni.id == NCKEY_LEFT) { + if (cursor_pos > 0) --cursor_pos; + } else if (ni.id == NCKEY_RIGHT) { + if (cursor_pos < input_buf.size()) ++cursor_pos; + } else if (ni.id == NCKEY_HOME) { + cursor_pos = 0; + } else if (ni.id == NCKEY_END) { + cursor_pos = input_buf.size(); + } else if (ni.id == NCKEY_UP) { + ++scroll_offset; redraw_chat(); notcurses_render(nc); continue; + } else if (ni.id == NCKEY_DOWN) { + if (scroll_offset > 0) --scroll_offset; + redraw_chat(); notcurses_render(nc); continue; + } else if (ni.id >= 32 && ni.id < 0xD800) { + input_buf.insert(cursor_pos, 1, (char)ni.id); + ++cursor_pos; + } + redraw_input(); + notcurses_render(nc); + } +} + +// ═══════════════════════════════════════════════════════════════════════════ +// AgentState β€” thin owner of Llama + LlamaIter + optional RAG objects. +// +// Design: +// β€’ Llama is created once; settings are applied before load_model(). +// β€’ iter is an std::optional so we can construct it lazily (LlamaIter has +// no default-construct-then-assign path once _has_next is false; we just +// move a fresh one in via add_message()). +// β€’ reset_conversation() calls llama.reset() which clears the KV cache, +// then re-injects the system prompt as the first message of the new turn. +// β€’ run_turn() mirrors the SB main() loop exactly: +// while iter.has_next β†’ next() β†’ accumulate line β†’ on TOOL: dispatch +// ═══════════════════════════════════════════════════════════════════════════ + +struct AgentState { + Llama llama; + + // iter is valid (has_next may be false) after the first add_message call. + // We use a pointer so it can be replaced by move. + std::unique_ptr iter; + + // Separate Llama instance for embeddings (optional) + std::unique_ptr embed_llama; + + // RAG objects + std::unique_ptr rag_db; + std::unique_ptr rag_session; + + bool model_loaded = false; + std::string system_prompt; + + // ── setup ───────────────────────────────────────────────────────── + bool setup_model(const NitroConfig &cfg, TuiState &tui); + bool setup_embed(const std::string &path, TuiState &tui); + void apply_generation_params(const NitroConfig &cfg); + + // ── conversation management ─────────────────────────────────────── + // Injects the system prompt as a fresh first turn. + // Call after setup_model() or whenever /clear is issued. + void reset_conversation(const std::string &sysprompt, TuiState &tui); + + // ── generation ──────────────────────────────────────────────────── + // Returns false on fatal error. + bool run_turn(const std::string &user_message, + const NitroConfig &cfg, + TuiState &tui); + + // ── RAG ─────────────────────────────────────────────────────────── + bool rag_index(const std::string &path, TuiState &tui); + + // ── status ──────────────────────────────────────────────────────── + std::string memory_info_text(); + + // Compute tok/s from iter (matches SB iter.tokens_sec() idiom) + float tokens_per_sec() const; +}; + +// ─── AgentState::setup_model ────────────────────────────────────────────── + +void AgentState::apply_generation_params(const NitroConfig &cfg) { + llama.add_stop("<|turn|>"); + llama.add_stop("<|im_end|>"); + llama.set_max_tokens(cfg.n_max_tokens); + llama.set_temperature(cfg.temperature); + llama.set_top_k(cfg.top_k); + llama.set_top_p(cfg.top_p); + llama.set_min_p(cfg.min_p); + llama.set_penalty_repeat(cfg.penalty_repeat); + llama.set_penalty_last_n(cfg.penalty_last_n); + llama.set_log_level(cfg.log_level); +} + +bool AgentState::setup_model(const NitroConfig &cfg, TuiState &tui) { + if (cfg.model_path.empty()) { + tui.append_line("[sys] No model loaded. Use /model to load a GGUF."); + tui.redraw_all(); + return false; + } + + // reset() clears any previous KV state cleanly + llama.reset(); + apply_generation_params(cfg); + + if (!llama.load_model(cfg.model_path, cfg.n_ctx, cfg.n_batch, + cfg.n_gpu_layers, cfg.log_level)) { + tui.append_line(std::string("[err] ") + llama.last_error()); + tui.redraw_all(); + return false; + } + + model_loaded = true; + tui.current_model = fs::path(cfg.model_path).filename().string(); + tui.append_line("[sys] Model ready: " + tui.current_model); + + // Show memory advice (mirrors SB: print GREEN + mem.advice) + LlamaMemoryInfo mem = llama.memory_info(); + tui.append_line("[sys] " + mem.advice); + tui.kv_used = mem.kv_used; + tui.kv_total = mem.kv_total; + tui.vram_used = mem.vram_used; + tui.vram_total = mem.vram_total; + tui.redraw_all(); + return true; +} + +bool AgentState::setup_embed(const std::string &path, TuiState &tui) { + tui.append_line("[sys] Loading embedding model: " + path); + tui.redraw_all(); + embed_llama = std::make_unique(); + if (!embed_llama->load_embedding_model(path)) { + tui.append_line(std::string("[err] ") + embed_llama->last_error()); + tui.redraw_all(); + embed_llama.reset(); + return false; + } + rag_db = std::make_unique(); + rag_session = std::make_unique(); + tui.append_line("[sys] Embedding model ready."); + tui.redraw_all(); + return true; +} + +// ─── AgentState::reset_conversation ────────────────────────────────────── +// Mirrors the SB pattern: +// local iter = llama.add_message("system", initialize_agent()) + +void AgentState::reset_conversation(const std::string &sysprompt, TuiState &tui) { + system_prompt = sysprompt; + llama.reset(); // clears KV cache + sampler state + apply_generation_params(NitroConfig{}); // re-apply stops / params after reset + + iter = std::make_unique(); + if (!llama.add_message(*iter, "system", system_prompt)) { + tui.append_line(std::string("[err] System prompt injection: ") + llama.last_error()); + tui.redraw_all(); + } +} + +// ─── AgentState::tokens_per_sec ────────────────────────────────────────── +// LlamaIter stores _t_start and _tokens_generated; we replicate the SB +// iter.tokens_sec() calculation here since LlamaIter doesn't expose it +// as a method in the public header. + +float AgentState::tokens_per_sec() const { + if (!iter) return 0.0f; + auto now = std::chrono::high_resolution_clock::now(); + double elapsed = std::chrono::duration(now - iter->_t_start).count(); + if (elapsed <= 0.0 || iter->_tokens_generated <= 0) return 0.0f; + return (float)(iter->_tokens_generated / elapsed); +} + +// ─── AgentState::memory_info_text ──────────────────────────────────────── + +std::string AgentState::memory_info_text() { + if (!model_loaded) return "No model loaded."; + LlamaMemoryInfo m = llama.memory_info(); + std::ostringstream oss; + oss << "KV cache : " << m.kv_used << " / " << m.kv_total + << " (" << m.kv_percent << "%)\n"; + if (m.vram_total > 0) { + oss << "VRAM : " << (m.vram_used >> 20) << " MB / " + << (m.vram_total >> 20) << " MB (" << m.vram_percent << "%)\n"; + } + oss << "GPU layers: " << m.n_layers_gpu << " / " << m.n_layers_total << "\n"; + oss << "CPU layers: " << m.n_layers_cpu << "\n"; + oss << "Advice : " << m.advice << "\n"; + return oss.str(); +} + +// ─── AgentState::rag_index ─────────────────────────────────────────────── + +bool AgentState::rag_index(const std::string &path, TuiState &tui) { + if (!embed_llama || !rag_db) { + tui.append_line("[err] Load an embedding model first: /embed "); + tui.redraw_all(); + return false; + } + + auto index_one = [&](const std::string &filepath) { + tui.append_line("[sys] indexing: " + filepath); + tui.redraw_all(); + if (!embed_llama->rag_load(*rag_db, filepath)) { + tui.append_line(std::string("[err] rag_load: ") + embed_llama->last_error()); + tui.redraw_all(); + } + }; + + fs::path rp(path); + std::error_code ec; + if (fs::is_directory(rp, ec)) { + for (const auto &entry : fs::recursive_directory_iterator(rp, ec)) { + if (entry.is_regular_file()) index_one(entry.path().string()); + } + } else { + index_one(path); + } + return true; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Agent turn β€” mirrors SB main() loop +// +// SB pattern (condensed): +// while iter.has_next() +// buffer += iter.next() +// if newline in buffer: +// line = left side of buffer +// if TOOL: β†’ line += buffer + iter.all() +// iter = llama.add_message("tool", process_tool(line)) +// else print line +// if remaining buffer is TOOL: β†’ process it +// else flush remaining buffer +// ═══════════════════════════════════════════════════════════════════════════ + +bool AgentState::run_turn(const std::string &user_message, + const NitroConfig &cfg, + TuiState &tui) { + if (!model_loaded) { + tui.append_line("[err] No model loaded. Use /model "); + tui.redraw_all(); + return false; + } + + // ── optional RAG context injection ─────────────────────────────── + // If we have a RAG session, prepend retrieved context to the user message. + std::string effective_message = user_message; + if (embed_llama && rag_db && rag_session) { + std::string context = llama.rag_retrieve(*rag_db, user_message, + cfg.rag_top_k, *rag_session); + if (!context.empty()) { + effective_message = "Context:\n" + context + "\n\nUser: " + user_message; + } + } + + // ── inject user message ─────────────────────────────────────────── + // iter must already exist (reset_conversation initialises it with "system"). + // add_message("user", …) appends to the existing KV context. + if (!iter) { + tui.append_line("[err] Conversation not initialised (call /clear to reset)"); + tui.redraw_all(); + return false; + } + + if (!llama.add_message(*iter, "user", effective_message)) { + tui.append_line(std::string("[err] add_message: ") + llama.last_error()); + tui.redraw_all(); + return false; + } + + // ── label the assistant response in the chat pane ──────────────── + tui.append_line("Nitro: "); + tui.redraw_all(); + + // ── generation loop ─────────────────────────────────────────────── + // Exact translation of the SB streaming / tool-dispatch loop. + bool in_think = false; + std::string buffer; // accumulates tokens until we see a newline + + auto handle_think = [&](const std::string &line) { + if (line == "<|think|>") in_think = true; + else if (line == "") in_think = false; + }; + + while (iter->_has_next) { + std::string tok = llama.next(*iter); + buffer += tok; + + auto nl = buffer.find('\n'); + if (nl != std::string::npos) { + std::string text_line = buffer.substr(0, nl); + buffer = buffer.substr(nl + 1); + + // Trim leading whitespace to detect TOOL: reliably + std::string trimmed = text_line; + trimmed.erase(0, trimmed.find_first_not_of(" \t")); + + if (trimmed.substr(0, 5) == "TOOL:") { + // Collect any tail that remains in the buffer plus iter.all() + // β€” mirrors: text_line += buffer + " " + iter.all() + std::string tool_line = trimmed + " " + buffer + " " + llama.all(*iter); + + // Strip stray newlines from the single-line tool command + tool_line.erase( + std::remove(tool_line.begin(), tool_line.end(), '\n'), + tool_line.end()); + + // Trim trailing whitespace + while (!tool_line.empty() && std::isspace((unsigned char)tool_line.back())) + tool_line.pop_back(); + + tui.append_line("[tool] " + tool_line); + tui.redraw_all(); + + std::string result = process_tool(tool_line, cfg.sandbox, tui); + + tui.append_line("[tool] β†’ " + + result.substr(0, 200) + (result.size() > 200 ? "…" : "")); + tui.redraw_all(); + + // Inject tool result and get a new iter for the continuation + // β€” mirrors: iter = llama.add_message("tool", process_tool(…)) + if (!llama.add_message(*iter, "tool", result)) { + tui.append_line(std::string("[err] tool result inject: ") + llama.last_error()); + tui.redraw_all(); + break; + } + buffer.clear(); + + } else { + // Normal output line + if (!in_think) { + tui.append_token(text_line + "\n"); + } + handle_think(text_line); + } + } + } + + // ── flush remaining buffer (SB: "Flush remaining line buffer") ──── + if (!buffer.empty()) { + std::string trimmed = buffer; + trimmed.erase(0, trimmed.find_first_not_of(" \t")); + + if (trimmed.substr(0, 5) == "TOOL:") { + std::string result = process_tool(trimmed, cfg.sandbox, tui); + tui.append_line("[tool] β†’ " + result.substr(0, 200)); + tui.redraw_all(); + llama.add_message(*iter, "tool", result); + } else { + if (!in_think) tui.append_token(buffer); + } + } + tui.flush_token_acc(); + + // ── update status bar ───────────────────────────────────────────── + tui.tokens_per_sec = tokens_per_sec(); + LlamaMemoryInfo mem = llama.memory_info(); + tui.kv_used = mem.kv_used; + tui.kv_total = mem.kv_total; + tui.vram_used = mem.vram_used; + tui.vram_total = mem.vram_total; + + // ── stat line (mirrors SB: "Tokens/sec: …") ────────────────────── + char stat[128]; + std::snprintf(stat, sizeof(stat), "[sys] %.1f tok/s (%d tokens) KV %.1f%%", + (double)tui.tokens_per_sec, + iter->_tokens_generated, + (double)mem.kv_percent); + tui.append_line(stat); + tui.redraw_all(); + return true; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// File-system helpers +// ═══════════════════════════════════════════════════════════════════════════ + +static std::string join_path(const std::string &a, const std::string &b) { + if (b.empty()) return a; + if (b[0] == '/') return b; + std::string pa = a; + if (!pa.empty() && pa.back() == '/') pa.pop_back(); + std::string pb = (b.front() == '/') ? b.substr(1) : b; + return pa + "/" + pb; +} + +static bool path_in_sandbox(const std::string &sandbox, const std::string &path) { + std::error_code ec; + auto base = fs::canonical(sandbox, ec); if (ec) return false; + auto target = fs::weakly_canonical(path, ec); + std::string bstr = base.string() + "/"; + std::string tstr = target.string(); + return tstr == base.string() || tstr.compare(0, bstr.size(), bstr) == 0; +} + +static std::string read_file(const std::string &path) { + std::ifstream f(path, std::ios::binary); + if (!f) return "ERROR: cannot open " + path; + std::ostringstream oss; oss << f.rdbuf(); + return oss.str(); +} + +static bool write_file(const std::string &path, const std::string &data) { + fs::path p(path); + if (p.has_parent_path()) { + std::error_code ec; + fs::create_directories(p.parent_path(), ec); + } + std::ofstream f(path, std::ios::binary | std::ios::trunc); + if (!f) return false; + f.write(data.data(), (std::streamsize)data.size()); + return f.good(); +} + +static std::string list_dir(const std::string &path) { + std::ostringstream oss; + std::error_code ec; + for (const auto &e : fs::directory_iterator(path, ec)) { + if (ec) break; + std::string name = e.path().filename().string(); + if (name.empty() || name[0] == '.') continue; + oss << (e.is_directory() ? "[" + name + "]" : name) << "\n"; + } + return oss.str(); +} + +static const std::vector CODE_EXTENSIONS = { + ".py",".c",".cpp",".h",".bas",".java",".html",".js",".ts", + ".json",".yaml",".toml",".sh",".go",".rs",".jsx",".tsx" +}; + +static std::string strip_code_fences(const std::string &filename, + const std::string &src) { + auto ext = fs::path(filename).extension().string(); + bool is_code = std::any_of(CODE_EXTENSIONS.begin(), CODE_EXTENSIONS.end(), + [&](const std::string &e){ return ext == e; }); + if (!is_code) return src; + auto pos = src.find("```"); + if (pos == std::string::npos) return src; + auto nl = src.find('\n', pos + 3); + if (nl == std::string::npos) return src; + std::string inner = src.substr(nl + 1); + auto end = inner.rfind("```"); + if (end != std::string::npos) inner = inner.substr(0, end); + return inner; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Tool dispatch (mirrors SB process_tool) +// ═══════════════════════════════════════════════════════════════════════════ + +static std::string process_tool(const std::string &cmd, + const std::string &sandbox, + TuiState &tui) { + // Parse: OP [ARG1 [REST…]] + std::string op, arg1, arg2; + auto sp1 = cmd.find(' '); + if (sp1 == std::string::npos) { + op = cmd; + } else { + op = cmd.substr(0, sp1); + std::string rest = cmd.substr(sp1 + 1); + // ltrim + rest.erase(0, rest.find_first_not_of(" \t")); + auto sp2 = rest.find(' '); + if (sp2 == std::string::npos) { + arg1 = rest; + } else { + arg1 = rest.substr(0, sp2); + arg2 = rest.substr(sp2 + 1); + } + } + + // Resolve arg1 into an absolute path inside the sandbox + auto resolve = [&](const std::string &p) -> std::string { + if (p.empty() || p == ".") return sandbox; + if (p.substr(0, 2) == "./") return join_path(sandbox, p.substr(2)); + if (p[0] == '/') return p; + return join_path(sandbox, p); + }; + + if (op == "TOOL:DATE") { + char buf[32]; time_t t = time(nullptr); + strftime(buf, sizeof(buf), "%Y-%m-%d", localtime(&t)); + return buf; + } + if (op == "TOOL:TIME") { + char buf[32]; time_t t = time(nullptr); + strftime(buf, sizeof(buf), "%H:%M:%S", localtime(&t)); + return buf; + } + if (op == "TOOL:RND") { + return std::to_string((double)rand() / RAND_MAX); + } + if (op == "TOOL:LIST") { + std::string dir = resolve(arg1); + if (!path_in_sandbox(sandbox, dir)) return "ERROR: path outside sandbox"; + return list_dir(dir); + } + if (op == "TOOL:EXISTS") { + std::string p = resolve(arg1); + if (!path_in_sandbox(sandbox, p)) return "NO"; + return fs::exists(p) ? "YES" : "NO"; + } + if (op == "TOOL:READ") { + std::string p = resolve(arg1); + if (!path_in_sandbox(sandbox, p)) return "ERROR: path outside sandbox"; + return read_file(p); + } + if (op == "TOOL:WRITE") { + std::string p = resolve(arg1); + if (!path_in_sandbox(sandbox, p)) return "ERROR: path outside sandbox"; + std::string content = strip_code_fences(arg1, arg2); + return write_file(p, content) ? "OK: written to " + arg1 + : "ERROR: write failed for " + arg1; + } + if (op == "TOOL:PERMISSION") { + std::string result; + tui.confirm_dialog("Allow model to proceed?", result); + return result; + } + if (op == "TOOL:RUN") { + std::string prog = resolve(arg1); + if (!path_in_sandbox(sandbox, prog)) return "ERROR: path outside sandbox"; + std::string command = prog + " " + arg2 + " 2>&1"; + FILE *fp = popen(command.c_str(), "r"); + if (!fp) return "ERROR: popen failed"; + std::string out; + char buf[256]; + while (fgets(buf, sizeof(buf), fp)) out += buf; + pclose(fp); + if (out.size() > 4096) out = out.substr(0, 4096) + "\n…(truncated)"; + return out; + } + + return "ERROR: unknown tool: " + op; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// System prompt (mirrors SB initialize_agent) +// ═══════════════════════════════════════════════════════════════════════════ + +static std::string build_system_prompt(const std::vector &knowledge_files, + const std::string &sandbox) { + std::string p; + p += "You are Nitro, an agentic AI assistant for software development.\n" + "Your sandbox (project directory) is: " + sandbox + "\n\n" + "## Tool protocol\n" + "Emit tool calls on their own line. The host executes them and returns\n" + "TOOL_RESULT: on the next line.\n\n" + "Available tools:\n" + " TOOL:LIST [dir] list files (default: sandbox root)\n" + " TOOL:READ read file contents\n" + " TOOL:WRITE write text to file\n" + " TOOL:EXISTS YES or NO\n" + " TOOL:RUN [args] run program inside sandbox\n" + " TOOL:DATE current date\n" + " TOOL:TIME current time\n" + " TOOL:RND random float\n" + " TOOL:PERMISSION ask user for explicit permission\n\n" + "Rules:\n" + "- Never access files outside the sandbox.\n" + "- Use TOOL:PERMISSION before destructive or irreversible operations.\n" + "- Reason step-by-step inside <|think|>… (hidden from user).\n" + "- After each tool call, explain what you did in plain English.\n\n"; + + for (const auto &kf : knowledge_files) { + std::ifstream f(kf); + if (!f) continue; + std::ostringstream oss; oss << f.rdbuf(); + p += "## Knowledge: " + kf + "\n" + oss.str() + "\n\n"; + } + return p; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Slash command handler +// ═══════════════════════════════════════════════════════════════════════════ + +static void handle_slash(const std::string &input, + NitroConfig &cfg, + AgentState &agent, + TuiState &tui) { + auto sp = input.find(' '); + std::string verb = (sp == std::string::npos) ? input : input.substr(0, sp); + std::string rest; + if (sp != std::string::npos) { + rest = input.substr(sp + 1); + rest.erase(0, rest.find_first_not_of(" \t")); + } + + if (verb == "/help") { + tui.append_line("[sys] Commands:"); + tui.append_line("[sys] /model load a GGUF model"); + tui.append_line("[sys] /embed load an embedding model for RAG"); + tui.append_line("[sys] /rag index file or directory"); + tui.append_line("[sys] /memory KV / VRAM / layer stats"); + tui.append_line("[sys] /clear reset conversation"); + tui.append_line("[sys] /help this message"); + tui.append_line("[sys] exit / quit exit Nitro"); + tui.redraw_all(); + return; + } + + if (verb == "/model") { + if (rest.empty()) { + tui.append_line("[err] Usage: /model "); + tui.redraw_all(); return; + } + cfg.model_path = rest; + if (agent.setup_model(cfg, tui)) { + std::string sysp = build_system_prompt(cfg.knowledge_files, cfg.sandbox); + agent.reset_conversation(sysp, tui); + } + tui.redraw_all(); + return; + } + + if (verb == "/embed") { + if (rest.empty()) { + tui.append_line("[err] Usage: /embed "); + tui.redraw_all(); return; + } + cfg.embed_path = rest; + agent.setup_embed(rest, tui); + return; + } + + if (verb == "/rag") { + if (rest.empty()) { + tui.append_line("[err] Usage: /rag "); + tui.redraw_all(); return; + } + agent.rag_index(rest, tui); + return; + } + + if (verb == "/memory") { + std::istringstream iss(agent.memory_info_text()); + std::string line; + while (std::getline(iss, line)) tui.append_line("[sys] " + line); + tui.redraw_all(); + return; + } + + if (verb == "/clear") { + { std::lock_guard lk(tui.lines_mutex); + tui.chat_lines.clear(); } + std::string sysp = build_system_prompt(cfg.knowledge_files, cfg.sandbox); + agent.reset_conversation(sysp, tui); + tui.append_line("[sys] Conversation cleared."); + tui.redraw_all(); + return; + } + + tui.append_line("[err] Unknown command: " + verb + " (try /help)"); + tui.redraw_all(); +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Welcome banner +// ═══════════════════════════════════════════════════════════════════════════ + +static void welcome(TuiState &tui, const std::string &sandbox) { + tui.append_line("[sys] ╔═══════════════════════════════════════════╗"); + tui.append_line("[sys] β•‘ N I T R O A G E N T v1.0 β•‘"); + tui.append_line("[sys] β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•"); + tui.append_line("[sys] Sandbox : " + sandbox); + tui.append_line("[sys] /help for commands Β· exit to quit"); + tui.append_line(""); + tui.redraw_all(); +} + +// ═══════════════════════════════════════════════════════════════════════════ +// main() +// ═══════════════════════════════════════════════════════════════════════════ + +int main(int argc, char **argv) { + NitroConfig cfg; + + // ── Parse arguments ─────────────────────────────────────────────── + // Accepted forms: + // ./nitro [options] [project_dir] + // -m / --model GGUF to load + // -e / --embed embedding model + // -g / --gpu-layers GPU layer count + // The first non-option argument is treated as project_dir. + + auto resolve_path = [](const std::string &arg) -> std::string { + std::error_code ec; + if (arg.substr(0, 2) == "~/") { + const char *home = getenv("HOME"); + return std::string(home ? home : ".") + "/" + arg.substr(2); + } + if (arg.substr(0, 2) == "./") + return (fs::current_path(ec) / arg.substr(2)).string(); + return arg; + }; + + for (int i = 1; i < argc; ++i) { + std::string a = argv[i]; + + auto take_next = [&](const char *flag) -> std::string { + if (i + 1 >= argc) { + std::fprintf(stderr, "nitro: %s requires an argument\n", flag); + std::exit(1); + } + return argv[++i]; + }; + + if (a == "-m" || a == "--model") { + cfg.model_path = resolve_path(take_next(a.c_str())); + } else if (a == "-e" || a == "--embed") { + cfg.embed_path = resolve_path(take_next(a.c_str())); + } else if (a == "-g" || a == "--gpu-layers") { + cfg.n_gpu_layers = std::stoi(take_next(a.c_str())); + } else if (a == "-h" || a == "--help") { + std::puts( + "Usage: nitro [options] [project_dir]\n" + "\n" + "Options:\n" + " -m, --model GGUF model to load on startup\n" + " -e, --embed embedding model for RAG\n" + " -g, --gpu-layers GPU layers to offload (default: 32)\n" + " -h, --help show this help\n" + "\n" + "project_dir defaults to the current working directory.\n" + "\n" + "Slash commands inside nitro:\n" + " /model load / hot-reload a GGUF\n" + " /embed load an embedding model\n" + " /rag index file or directory\n" + " /memory KV / VRAM / layer stats\n" + " /clear reset conversation\n" + " /help list commands\n" + ); + return 0; + } else if (!a.empty() && a[0] == '-') { + std::fprintf(stderr, "nitro: unknown option '%s' (try --help)\n", a.c_str()); + std::exit(1); + } else { + // positional β†’ project_dir + cfg.sandbox = resolve_path(a); + } + } + + // ── Resolve sandbox ─────────────────────────────────────────────── + if (cfg.sandbox.empty()) { + std::error_code ec; + cfg.sandbox = fs::current_path(ec).string(); + } + { std::error_code ec; fs::create_directories(cfg.sandbox, ec); } + + // ── Auto-discover knowledge files ───────────────────────────────── + for (const char *kf : {"nitro.md", "AGENTS.md", "README.md"}) { + if (fs::exists(kf)) cfg.knowledge_files.push_back(kf); + } + + // ── Init TUI ────────────────────────────────────────────────────── + TuiState tui; + tui.init(); + welcome(tui, cfg.sandbox); + + // ── Init agent ──────────────────────────────────────────────────── + // AgentState owns a Llama whose constructor calls llama_backend_init(); + // its destructor calls llama_backend_free() β€” nitro never touches + // the raw llama API directly. + AgentState agent; + + if (!cfg.model_path.empty()) { + // Model provided on the command line β€” load immediately. + if (agent.setup_model(cfg, tui)) { + std::string sysp = build_system_prompt(cfg.knowledge_files, cfg.sandbox); + agent.reset_conversation(sysp, tui); + } + // Load embedding model if also provided up-front. + if (!cfg.embed_path.empty()) + agent.setup_embed(cfg.embed_path, tui); + } else { + // No model yet β€” friendly prompt, not an error. + tui.append_line("[sys] No model specified. Use /model to load one."); + tui.append_line("[sys] Example: /model ~/models/qwen2.5-7b-q4_k_m.gguf"); + tui.redraw_all(); + } + + // ── Main loop ───────────────────────────────────────────────────── + for (;;) { + // Check for terminal resize + { + unsigned rows = 0, cols = 0; + notcurses_stddim_yx(tui.nc, &rows, &cols); + if ((int)rows != tui.term_rows || (int)cols != tui.term_cols) + tui.resize(); + } + + std::string input = tui.readline_blocking(); + // trim + input.erase(0, input.find_first_not_of(" \t")); + if (!input.empty()) + input.erase(input.find_last_not_of(" \t\r\n") + 1); + if (input.empty()) continue; + + tui.append_line("You: " + input); + tui.redraw_all(); + + if (input == "exit" || input == "quit") break; + + if (input[0] == '/') { + handle_slash(input, cfg, agent, tui); + } else { + agent.run_turn(input, cfg, tui); + } + } + + tui.destroy(); + // agent destructor cleans up Llama (which calls llama_backend_free) + return 0; +} From 370e440b9a672ebb61167c3aee839d85b731304d Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Wed, 20 May 2026 20:21:38 +0930 Subject: [PATCH 110/131] LLAMA: replace test_main with nitro agent application --- llama/CMakeLists.txt | 75 ++++++++++---- llama/llama-sb-rag.cpp | 50 +-------- llama/llama-sb-rag.h | 58 +++++++++++ llama/llama-sb.cpp | 4 - llama/llama.cpp | 2 +- llama/nitro.cpp | 223 ++++++++++++++++++++++++++++++++--------- llama/test_main.cpp | 74 -------------- 7 files changed, 294 insertions(+), 192 deletions(-) create mode 100644 llama/llama-sb-rag.h delete mode 100644 llama/test_main.cpp diff --git a/llama/CMakeLists.txt b/llama/CMakeLists.txt index 749b55e..282337b 100644 --- a/llama/CMakeLists.txt +++ b/llama/CMakeLists.txt @@ -150,27 +150,68 @@ set_target_properties(llm PROPERTIES ) # ----------------------------- -# Optional test application +# nitro agent application +# (only built when notcurses is available) # ----------------------------- -add_executable(llm_test - test_main.cpp -) +find_package(PkgConfig QUIET) + +set(NC_FOUND FALSE) +set(NC_TARGET "") + +if(DEFINED NOTCURSES_DIR) + # Explicit path β€” create an imported target manually + find_library(NC_LIB NAMES notcurses HINTS "${NOTCURSES_DIR}/lib" REQUIRED) + find_library(NC_CORE_LIB NAMES notcurses-core HINTS "${NOTCURSES_DIR}/lib") + add_library(notcurses_imported INTERFACE IMPORTED) + target_include_directories(notcurses_imported INTERFACE "${NOTCURSES_DIR}/include") + target_link_libraries(notcurses_imported INTERFACE ${NC_LIB}) + if(NC_CORE_LIB) + target_link_libraries(notcurses_imported INTERFACE ${NC_CORE_LIB}) + endif() + set(NC_TARGET notcurses_imported) + set(NC_FOUND TRUE) + +elseif(PkgConfig_FOUND) + # IMPORTED_TARGET gives a PkgConfig::NC target with full lib paths baked in + pkg_check_modules(NC QUIET IMPORTED_TARGET notcurses) + if(NC_FOUND) + set(NC_TARGET PkgConfig::NC) + endif() -target_include_directories(llm_test PRIVATE - ${LLAMA_DIR}/include - ${LLAMA_DIR}/ggml/include - ${CMAKE_CURRENT_SOURCE_DIR}/../include -) +else() + find_library(NC_LIB NAMES notcurses) + find_library(NC_CORE_LIB NAMES notcurses-core) + if(NC_LIB AND NC_CORE_LIB) + add_library(notcurses_imported INTERFACE IMPORTED) + target_link_libraries(notcurses_imported INTERFACE ${NC_LIB} ${NC_CORE_LIB}) + set(NC_TARGET notcurses_imported) + set(NC_FOUND TRUE) + endif() +endif() -target_link_libraries(llm_test PRIVATE - llm - llama - ggml -) +if(NC_FOUND) + message(STATUS "notcurses found β€” building nitro") -set_target_properties(llm_test PROPERTIES - RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin -) + add_executable(nitro + nitro.cpp + ) + target_include_directories(nitro PRIVATE + ${LLAMA_DIR}/include + ${LLAMA_DIR}/ggml/include + ${CMAKE_CURRENT_SOURCE_DIR}/../include + ) + target_link_libraries(nitro PRIVATE + llm + llama + ggml + ${NC_TARGET} # imported target carries include + lib paths + ) + set_target_properties(nitro PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin + ) +else() + message(STATUS "notcurses not found β€” skipping nitro (set -DNOTCURSES_DIR=... to enable)") +endif() # ----------------------------- # RAG indexer diff --git a/llama/llama-sb-rag.cpp b/llama/llama-sb-rag.cpp index 77c08b3..a64f31f 100644 --- a/llama/llama-sb-rag.cpp +++ b/llama/llama-sb-rag.cpp @@ -6,6 +6,7 @@ // Copyright(C) 2026 Chris Warren-Smith #include "llama-sb.h" +#include "llama-sb-rag.h" #include #include @@ -17,55 +18,6 @@ #include #include -struct RagChunk { - std::string text; - std::string source; - std::string type; - std::vector embedding; -}; - -struct RagDB { - std::vector chunks; - int embed_dim = 0; - - int size() const { return (int)chunks.size(); } - bool empty() const { return chunks.empty(); } -}; - -// -// per-session deduplication + token budget -// -struct RagSession { - std::vector seen; /* sized to db.size() on init */ - int tokens_used = 0; - int tokens_max = 0; /* set to your n_ctx */ - float score_threshold = 0.60f; /* skip weak matches */ - - void init(int n_chunks, int ctx_size) { - seen.assign(n_chunks, false); - tokens_used = 0; - tokens_max = ctx_size; - } - - void reset() { - std::fill(seen.begin(), seen.end(), false); - tokens_used = 0; - } - - bool is_seen(int idx) const { return idx < (int)seen.size() && seen[idx]; } - void mark(int idx) { if (idx < (int)seen.size()) seen[idx] = true; } - - /* rough token estimate: 1 token β‰ˆ 4 chars */ - bool budget_ok(const std::string &text) const { - return tokens_max == 0 || - (tokens_used + (int)text.size() / 4) < (int)(tokens_max * 0.85f); - } - - void charge(const std::string &text) { - tokens_used += (int)text.size() / 4; - } -}; - bool Llama::embed_text(const std::string &text, std::vector &out, int embed_dim) { vector tokens = tokenize(text); if (tokens.size() == 0) { diff --git a/llama/llama-sb-rag.h b/llama/llama-sb-rag.h new file mode 100644 index 0000000..d31706c --- /dev/null +++ b/llama/llama-sb-rag.h @@ -0,0 +1,58 @@ +// This file is part of SmallBASIC +// +// This program is distributed under the terms of the GPL v2.0 or later +// Download the GNU Public License (GPL) from www.gnu.org +// +// Copyright(C) 2026 Chris Warren-Smith + +#pragma once + +struct RagChunk { + std::string text; + std::string source; + std::string type; + std::vector embedding; +}; + +struct RagDB { + std::vector chunks; + int embed_dim = 0; + + int size() const { return (int)chunks.size(); } + bool empty() const { return chunks.empty(); } +}; + +// +// per-session deduplication + token budget +// +struct RagSession { + std::vector seen; /* sized to db.size() on init */ + int tokens_used = 0; + int tokens_max = 0; /* set to your n_ctx */ + float score_threshold = 0.60f; /* skip weak matches */ + + void init(int n_chunks, int ctx_size) { + seen.assign(n_chunks, false); + tokens_used = 0; + tokens_max = ctx_size; + } + + void reset() { + std::fill(seen.begin(), seen.end(), false); + tokens_used = 0; + } + + bool is_seen(int idx) const { return idx < (int)seen.size() && seen[idx]; } + void mark(int idx) { if (idx < (int)seen.size()) seen[idx] = true; } + + /* rough token estimate: 1 token β‰ˆ 4 chars */ + bool budget_ok(const std::string &text) const { + return tokens_max == 0 || + (tokens_used + (int)text.size() / 4) < (int)(tokens_max * 0.85f); + } + + void charge(const std::string &text) { + tokens_used += (int)text.size() / 4; + } +}; + diff --git a/llama/llama-sb.cpp b/llama/llama-sb.cpp index 9e3b54a..6a74c69 100644 --- a/llama/llama-sb.cpp +++ b/llama/llama-sb.cpp @@ -130,10 +130,6 @@ void Llama::reset() { _min_p = 0.0f; _max_tokens = 150; _n_past = 0; - _is_gemma4 = false; - _grammar_src.clear(); - _grammar_root.clear(); - _template.clear(); _seed = LLAMA_DEFAULT_SEED; if (_ctx) { llama_memory_clear(llama_get_memory(_ctx), true); diff --git a/llama/llama.cpp b/llama/llama.cpp index 3fbadb0..6db1304 160000 --- a/llama/llama.cpp +++ b/llama/llama.cpp @@ -1 +1 @@ -Subproject commit 3fbadb06dc867d3236937705477e090724ebbc6e +Subproject commit 6db130445d29b243ee2171efb8cd61b84a1c5322 diff --git a/llama/nitro.cpp b/llama/nitro.cpp index 6057ff2..6e82149 100644 --- a/llama/nitro.cpp +++ b/llama/nitro.cpp @@ -148,6 +148,14 @@ struct TuiState { int term_rows = 0; int term_cols = 0; + // ── thinking spinner ────────────────────────────────────────────── + bool thinking = false; + int spinner_frame = 0; + // Advance spinner by one frame and redraw the header. + void tick_spinner(); + // Toggle thinking mode; redraws header immediately. + void set_thinking(bool on); + // ── lifecycle ───────────────────────────────────────────────────── void init(); void destroy(); @@ -175,10 +183,26 @@ struct TuiState { }; // ─── colour helpers ────────────────────────────────────────────────────── +// Our dark background colours (must match ncplane_set_base values in init). +static constexpr uint32_t BG_CHAT_R = 18, BG_CHAT_G = 22, BG_CHAT_B = 30; +static constexpr uint32_t BG_INP_R = 22, BG_INP_G = 28, BG_INP_B = 38; +static constexpr uint32_t BG_HDR_R = 30, BG_HDR_G = 40, BG_HDR_B = 55; +// fg only (use only where bg is already set via ncplane_set_base) static inline uint64_t fg_rgb(uint32_t r, uint32_t g, uint32_t b) { return NCCHANNELS_INITIALIZER(r, g, b, 0, 0, 0); } +// fg + explicit bg β€” use this for all ncplane_set_channels calls so the +// background behind each glyph matches the plane's base colour exactly. +static inline uint64_t chat_ch(uint32_t r, uint32_t g, uint32_t b) { + return NCCHANNELS_INITIALIZER(r, g, b, BG_CHAT_R, BG_CHAT_G, BG_CHAT_B); +} +static inline uint64_t inp_ch(uint32_t r, uint32_t g, uint32_t b) { + return NCCHANNELS_INITIALIZER(r, g, b, BG_INP_R, BG_INP_G, BG_INP_B); +} +static inline uint64_t hdr_ch(uint32_t r, uint32_t g, uint32_t b) { + return NCCHANNELS_INITIALIZER(r, g, b, BG_HDR_R, BG_HDR_G, BG_HDR_B); +} // ─── TuiState::init ────────────────────────────────────────────────────── @@ -191,6 +215,13 @@ void TuiState::init() { stdpl = notcurses_stdplane(nc); notcurses_term_dim_yx(nc, (unsigned *)&term_rows, (unsigned *)&term_cols); + // Fill the entire terminal with our dark background before creating + // child planes β€” eliminates the "terminal colour showing through" artefact. + uint64_t bg = NCCHANNELS_INITIALIZER(BG_CHAT_R, BG_CHAT_G, BG_CHAT_B, + BG_CHAT_R, BG_CHAT_G, BG_CHAT_B); + ncplane_set_base(stdpl, " ", 0, bg); + ncplane_erase(stdpl); + // Header: row 0 ncplane_options hopt{}; hopt.y = 0; hopt.x = 0; @@ -203,12 +234,18 @@ void TuiState::init() { copt.y = 1; copt.x = 0; copt.rows = (unsigned)chat_rows; copt.cols = (unsigned)term_cols; chatpl = ncplane_create(stdpl, &copt); + ncplane_set_base(chatpl, " ", 0, + NCCHANNELS_INITIALIZER(BG_CHAT_R, BG_CHAT_G, BG_CHAT_B, + BG_CHAT_R, BG_CHAT_G, BG_CHAT_B)); // Input pane: last 2 rows ncplane_options iopt{}; iopt.y = term_rows - 2; iopt.x = 0; iopt.rows = 2; iopt.cols = (unsigned)term_cols; inputpl = ncplane_create(stdpl, &iopt); + ncplane_set_base(inputpl, " ", 0, + NCCHANNELS_INITIALIZER(BG_INP_R, BG_INP_G, BG_INP_B, + BG_INP_R, BG_INP_G, BG_INP_B)); redraw_all(); } @@ -231,19 +268,25 @@ void TuiState::resize() { void TuiState::redraw_header() { ncplane_erase(header); - ncplane_set_base(header, " ", 0, fg_rgb(30, 40, 55)); + ncplane_set_base(header, " ", 0, + NCCHANNELS_INITIALIZER(BG_HDR_R, BG_HDR_G, BG_HDR_B, + BG_HDR_R, BG_HDR_G, BG_HDR_B)); float kv_pct = kv_total > 0 ? 100.f * (float)kv_used / (float)kv_total : 0.f; float vram_pct = vram_total > 0 ? 100.f * (float)vram_used / (float)vram_total : 0.f; + // Spinner: braille dots rotate smoothly, clearly visible on the header + static const char *const SPIN[] = { "β£Ύ","β£½","β£»","β’Ώ","β‘Ώ","⣟","β£―","β£·" }; + const char *spin_str = thinking ? SPIN[spinner_frame % 8] : " "; + char buf[512]; int n = std::snprintf(buf, sizeof(buf), - " ✦ NITRO β”‚ %-32s β”‚ %5.1f tok/s β”‚ KV %4.1f%% VRAM %4.1f%%", + " ✦ NITRO β”‚ %-32s β”‚ %5.1f tok/s β”‚ KV %4.1f%% VRAM %4.1f%% %s", current_model.c_str(), (double)tokens_per_sec, - (double)kv_pct, (double)vram_pct); + (double)kv_pct, (double)vram_pct, spin_str); if (n > term_cols) buf[term_cols] = '\0'; - ncplane_set_channels(header, fg_rgb(130, 220, 200)); + ncplane_set_channels(header, hdr_ch(130, 220, 200)); ncplane_putstr_yx(header, 0, 0, buf); } @@ -262,12 +305,12 @@ void TuiState::redraw_chat() { const std::string &line = chat_lines[i]; uint64_t ch; - if (line.rfind("You: ", 0) == 0) ch = fg_rgb(100, 200, 255); - else if (line.rfind("Nitro: ", 0) == 0) ch = fg_rgb(180, 255, 180); - else if (line.rfind("[tool]", 0) == 0) ch = fg_rgb(255, 180, 80); - else if (line.rfind("[err]", 0) == 0) ch = fg_rgb(255, 80, 80); - else if (line.rfind("[sys]", 0) == 0) ch = fg_rgb(140, 140, 200); - else ch = fg_rgb(210, 210, 210); + if (line.rfind("You: ", 0) == 0) ch = chat_ch(100, 200, 255); + else if (line.rfind("Nitro: ", 0) == 0) ch = chat_ch(180, 255, 180); + else if (line.rfind("[tool]", 0) == 0) ch = chat_ch(255, 180, 80); + else if (line.rfind("[err]", 0) == 0) ch = chat_ch(255, 80, 80); + else if (line.rfind("[sys]", 0) == 0) ch = chat_ch(140, 140, 200); + else ch = chat_ch(210, 210, 210); ncplane_set_channels(chatpl, ch); std::string display = line.size() > cols ? line.substr(0, cols) : line; @@ -279,24 +322,51 @@ void TuiState::redraw_input() { ncplane_erase(inputpl); // Separator - ncplane_set_channels(inputpl, fg_rgb(80, 120, 160)); - std::string sep(term_cols, '-'); + ncplane_set_channels(inputpl, inp_ch(80, 120, 160)); + std::string sep(term_cols, '─'); ncplane_putstr_yx(inputpl, 0, 0, sep.c_str()); - // Prompt + buffer + // Prompt const std::string prompt = " ❯ "; - ncplane_set_channels(inputpl, fg_rgb(230, 230, 230)); + const int prompt_cols = 4; + ncplane_set_channels(inputpl, inp_ch(100, 210, 255)); ncplane_putstr_yx(inputpl, 1, 0, prompt.c_str()); - int max_w = std::max(0, term_cols - (int)prompt.size() - 1); - std::string display = input_buf; - if ((int)display.size() > max_w && max_w > 0) - display = display.substr(display.size() - max_w); - ncplane_putstr_yx(inputpl, 1, (int)prompt.size(), display.c_str()); + // Buffer β€” split at cursor so we can render the cursor cell distinctly + int max_w = std::max(0, term_cols - prompt_cols - 1); - // Cursor position - int cx = std::min((int)prompt.size() + (int)cursor_pos, term_cols - 1); - ncplane_cursor_move_yx(inputpl, 1, cx); + // Viewport: if buffer is wider than the available space, show the tail + std::string visible = input_buf; + int view_offset = 0; + if ((int)visible.size() > max_w && max_w > 0) { + view_offset = (int)visible.size() - max_w; + visible = visible.substr(view_offset); + } + + // Text before cursor + int cur_in_view = std::max(0, (int)cursor_pos - view_offset); + cur_in_view = std::min(cur_in_view, (int)visible.size()); + + std::string before = visible.substr(0, cur_in_view); + std::string after = cur_in_view < (int)visible.size() + ? visible.substr(cur_in_view + 1) : ""; + char cursor_ch = cur_in_view < (int)visible.size() + ? visible[cur_in_view] : ' '; + + ncplane_set_channels(inputpl, inp_ch(230, 230, 230)); + ncplane_putstr_yx(inputpl, 1, prompt_cols, before.c_str()); + + // Cursor cell: bright bg, dark text β€” stands out against the input bg + int cx = prompt_cols + cur_in_view; + ncplane_set_channels(inputpl, + NCCHANNELS_INITIALIZER(BG_INP_R, BG_INP_G, BG_INP_B, 180, 230, 255)); + char cbuf[2] = { cursor_ch, '\0' }; + ncplane_putstr_yx(inputpl, 1, cx, cbuf); + + // Text after cursor + ncplane_set_channels(inputpl, inp_ch(230, 230, 230)); + if (!after.empty()) + ncplane_putstr_yx(inputpl, 1, cx + 1, after.c_str()); } void TuiState::redraw_all() { @@ -306,6 +376,19 @@ void TuiState::redraw_all() { notcurses_render(nc); } +void TuiState::tick_spinner() { + ++spinner_frame; + redraw_header(); + notcurses_render(nc); +} + +void TuiState::set_thinking(bool on) { + thinking = on; + if (!on) spinner_frame = 0; + redraw_header(); + notcurses_render(nc); +} + // ─── TuiState content helpers ───────────────────────────────────────────── void TuiState::append_line(const std::string &line) { @@ -344,7 +427,7 @@ void TuiState::flush_token_acc() { void TuiState::confirm_dialog(const std::string &prompt, std::string &result) { ncplane_erase(inputpl); - ncplane_set_channels(inputpl, fg_rgb(255, 200, 80)); + ncplane_set_channels(inputpl, inp_ch(255, 200, 80)); std::string msg = " " + prompt + " [y/n] ❯ "; ncplane_putstr_yx(inputpl, 1, 0, msg.c_str()); notcurses_render(nc); @@ -358,7 +441,7 @@ void TuiState::confirm_dialog(const std::string &prompt, std::string &result) { else if (ni.id >= 32 && ni.id < 127) { answer += (char)ni.id; } ncplane_erase(inputpl); - ncplane_set_channels(inputpl, fg_rgb(255, 200, 80)); + ncplane_set_channels(inputpl, inp_ch(255, 200, 80)); ncplane_putstr_yx(inputpl, 1, 0, (msg + answer).c_str()); notcurses_render(nc); } @@ -663,20 +746,31 @@ bool AgentState::run_turn(const std::string &user_message, // ── label the assistant response in the chat pane ──────────────── tui.append_line("Nitro: "); - tui.redraw_all(); + tui.set_thinking(true); // ── generation loop ─────────────────────────────────────────────── - // Exact translation of the SB streaming / tool-dispatch loop. - bool in_think = false; + bool in_think = false; std::string buffer; // accumulates tokens until we see a newline - auto handle_think = [&](const std::string &line) { - if (line == "<|think|>") in_think = true; - else if (line == "") in_think = false; + // Scan buffer for think open/close tags anywhere in the text. + // Models use either or <|think|> depending on the template. + auto update_think_state = [&](const std::string &text) { + // Opening tags + if (text.find("") != std::string::npos || + text.find("<|think|>") != std::string::npos) in_think = true; + // Closing tags β€” check after open so a single-line … ends correctly + if (text.find("") != std::string::npos || + text.find("") != std::string::npos) in_think = false; }; while (iter->_has_next) { std::string tok = llama.next(*iter); + tui.tick_spinner(); + + // Update think state on every token so tags that arrive mid-buffer + // suppress display immediately rather than waiting for a newline. + update_think_state(tok); + buffer += tok; auto nl = buffer.find('\n'); @@ -689,20 +783,57 @@ bool AgentState::run_turn(const std::string &user_message, trimmed.erase(0, trimmed.find_first_not_of(" \t")); if (trimmed.substr(0, 5) == "TOOL:") { - // Collect any tail that remains in the buffer plus iter.all() - // β€” mirrors: text_line += buffer + " " + iter.all() - std::string tool_line = trimmed + " " + buffer + " " + llama.all(*iter); - - // Strip stray newlines from the single-line tool command - tool_line.erase( - std::remove(tool_line.begin(), tool_line.end(), '\n'), - tool_line.end()); + // Collect remainder: rest of buffer + everything iter still has. + // For TOOL:WRITE the content may contain newlines, so we keep + // the raw text and only strip newlines from the op+arg1 prefix. + std::string tail = buffer + llama.all(*iter); + + // Parse op and arg1 from trimmed (first two space-separated tokens) + // then treat everything after as the raw payload (preserving newlines). + std::string op, arg1, payload; + { + auto s1 = trimmed.find(' '); + if (s1 != std::string::npos) { + op = trimmed.substr(0, s1); + std::string rest = trimmed.substr(s1 + 1); + rest.erase(0, rest.find_first_not_of(" \t")); + auto s2 = rest.find(' '); + if (s2 != std::string::npos) { + arg1 = rest.substr(0, s2); + payload = rest.substr(s2 + 1) + tail; + } else { + arg1 = rest; + payload = tail; + } + } else { + op = trimmed; + } + } - // Trim trailing whitespace - while (!tool_line.empty() && std::isspace((unsigned char)tool_line.back())) - tool_line.pop_back(); + // Reconstruct the tool line. For ops that don't carry a file payload + // (LIST, EXISTS, READ, DATE, TIME, RND, PERMISSION, RUN) we still + // collapse newlines in payload so the single-line format is preserved. + // For TOOL:WRITE we keep newlines in the payload intact. + std::string tool_line; + if (op == "TOOL:WRITE") { + tool_line = op + " " + arg1 + " " + payload; + // Trim only trailing whitespace + while (!tool_line.empty() && tool_line.back() == '\n') + tool_line.pop_back(); + } else { + tool_line = op; + if (!arg1.empty()) tool_line += " " + arg1; + if (!payload.empty()) { + std::string flat = payload; + flat.erase(std::remove(flat.begin(), flat.end(), '\n'), flat.end()); + while (!flat.empty() && std::isspace((unsigned char)flat.back())) + flat.pop_back(); + if (!flat.empty()) tool_line += " " + flat; + } + } - tui.append_line("[tool] " + tool_line); + tui.append_line("[tool] " + op + " " + arg1 + + (op == "TOOL:WRITE" ? " " : "")); tui.redraw_all(); std::string result = process_tool(tool_line, cfg.sandbox, tui); @@ -711,8 +842,6 @@ bool AgentState::run_turn(const std::string &user_message, result.substr(0, 200) + (result.size() > 200 ? "…" : "")); tui.redraw_all(); - // Inject tool result and get a new iter for the continuation - // β€” mirrors: iter = llama.add_message("tool", process_tool(…)) if (!llama.add_message(*iter, "tool", result)) { tui.append_line(std::string("[err] tool result inject: ") + llama.last_error()); tui.redraw_all(); @@ -721,16 +850,15 @@ bool AgentState::run_turn(const std::string &user_message, buffer.clear(); } else { - // Normal output line + // Normal output line β€” suppress if inside think block if (!in_think) { tui.append_token(text_line + "\n"); } - handle_think(text_line); } } } - // ── flush remaining buffer (SB: "Flush remaining line buffer") ──── + // ── flush remaining buffer ──────────────────────────────────────── if (!buffer.empty()) { std::string trimmed = buffer; trimmed.erase(0, trimmed.find_first_not_of(" \t")); @@ -745,6 +873,7 @@ bool AgentState::run_turn(const std::string &user_message, } } tui.flush_token_acc(); + tui.set_thinking(false); // ── update status bar ───────────────────────────────────────────── tui.tokens_per_sec = tokens_per_sec(); diff --git a/llama/test_main.cpp b/llama/test_main.cpp deleted file mode 100644 index 3ab572d..0000000 --- a/llama/test_main.cpp +++ /dev/null @@ -1,74 +0,0 @@ -#include "llama-sb.h" -#include -#include - -static void print_usage(int, char ** argv) { - printf("\nexample usage:\n"); - printf("\n %s -m model.gguf [-n n_predict] [-ngl n_gpu_layers] [prompt]\n", argv[0]); - printf("\n"); -} - -int main(int argc, char ** argv) { - // path to the model gguf file - std::string model_path; - // prompt to generate text from - std::string prompt = "Happy friday"; - // number of tokens to predict - int n_predict = 32; - - // parse command line arguments - int i = 1; - for (; i < argc; i++) { - if (strcmp(argv[i], "-m") == 0) { - if (i + 1 < argc) { - model_path = argv[++i]; - } else { - print_usage(argc, argv); - return 1; - } - } else if (strcmp(argv[i], "-n") == 0) { - if (i + 1 < argc) { - try { - n_predict = std::stoi(argv[++i]); - } catch (...) { - print_usage(argc, argv); - return 1; - } - } else { - print_usage(argc, argv); - return 1; - } - } else { - // prompt starts here - break; - } - } - if (model_path.empty()) { - print_usage(argc, argv); - return 1; - } - if (i < argc) { - prompt = argv[i++]; - for (; i < argc; i++) { - prompt += " "; - prompt += argv[i]; - } - } - - Llama llama; - if (llama.load_model(model_path, 1024, 1024, -1, GGML_LOG_LEVEL_CONT)) { - LlamaIter iter; - llama.set_max_tokens(n_predict); - llama.add_message(iter, "user", prompt); - while (iter._has_next) { - auto out = llama.next(iter); - printf("\033[33m"); - printf("%s\n", out.c_str()); - printf("\n\033[0m"); - } - } else { - fprintf(stderr, "ERR: %s\n", llama.last_error()); - } - - return 0; -} From 7f3e2ce29b33c2ad4a51ece29fc7d82d27209b14 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Thu, 21 May 2026 09:47:19 +0930 Subject: [PATCH 111/131] LLAMA: replace test_main with nitro agent application --- llama/CMakeLists.txt | 81 ++-- llama/nitro.cpp | 1090 +++++++++++++++++++++++++++++++----------- 2 files changed, 874 insertions(+), 297 deletions(-) diff --git a/llama/CMakeLists.txt b/llama/CMakeLists.txt index 282337b..ef68b0f 100644 --- a/llama/CMakeLists.txt +++ b/llama/CMakeLists.txt @@ -155,9 +155,9 @@ set_target_properties(llm PROPERTIES # ----------------------------- find_package(PkgConfig QUIET) +# ── notcurses ───────────────────────────────────────────────────────────── set(NC_FOUND FALSE) set(NC_TARGET "") - if(DEFINED NOTCURSES_DIR) # Explicit path β€” create an imported target manually find_library(NC_LIB NAMES notcurses HINTS "${NOTCURSES_DIR}/lib" REQUIRED) @@ -170,14 +170,11 @@ if(DEFINED NOTCURSES_DIR) endif() set(NC_TARGET notcurses_imported) set(NC_FOUND TRUE) - elseif(PkgConfig_FOUND) - # IMPORTED_TARGET gives a PkgConfig::NC target with full lib paths baked in pkg_check_modules(NC QUIET IMPORTED_TARGET notcurses) if(NC_FOUND) set(NC_TARGET PkgConfig::NC) endif() - else() find_library(NC_LIB NAMES notcurses) find_library(NC_CORE_LIB NAMES notcurses-core) @@ -189,9 +186,55 @@ else() endif() endif() +# ── libcurl ─────────────────────────────────────────────────────────────── +# Try the modern CMake find-module first (ships with CMake β‰₯ 3.12). +# Fall back to pkg-config, then a raw library search. +set(CURL_FOUND_INTERNAL FALSE) +set(CURL_TARGET "") + +if(DEFINED CURL_DIR) + # Explicit path supplied by the user (-DCURL_DIR=...) + find_library(CURL_LIB NAMES curl HINTS "${CURL_DIR}/lib" REQUIRED) + find_path(CURL_INCLUDE NAMES curl/curl.h HINTS "${CURL_DIR}/include" REQUIRED) + add_library(curl_imported INTERFACE IMPORTED) + target_include_directories(curl_imported INTERFACE "${CURL_INCLUDE}") + target_link_libraries(curl_imported INTERFACE ${CURL_LIB}) + set(CURL_TARGET curl_imported) + set(CURL_FOUND_INTERNAL TRUE) +else() + # CMake built-in (sets CURL::libcurl target when found) + find_package(CURL QUIET) + if(CURL_FOUND) + set(CURL_TARGET CURL::libcurl) + set(CURL_FOUND_INTERNAL TRUE) + elseif(PkgConfig_FOUND) + pkg_check_modules(CURL_PC QUIET IMPORTED_TARGET libcurl) + if(CURL_PC_FOUND) + set(CURL_TARGET PkgConfig::CURL_PC) + set(CURL_FOUND_INTERNAL TRUE) + endif() + endif() + if(NOT CURL_FOUND_INTERNAL) + find_library(CURL_LIB NAMES curl) + find_path(CURL_INCLUDE NAMES curl/curl.h) + if(CURL_LIB AND CURL_INCLUDE) + add_library(curl_imported INTERFACE IMPORTED) + target_include_directories(curl_imported INTERFACE "${CURL_INCLUDE}") + target_link_libraries(curl_imported INTERFACE ${CURL_LIB}) + set(CURL_TARGET curl_imported) + set(CURL_FOUND_INTERNAL TRUE) + endif() + endif() +endif() + +# ── nitro target ────────────────────────────────────────────────────────── if(NC_FOUND) - message(STATUS "notcurses found β€” building nitro") + if(NOT CURL_FOUND_INTERNAL) + message(WARNING "libcurl not found β€” TOOL:CURL will be unavailable. " + "Install libcurl-dev or set -DCURL_DIR= to enable.") + endif() + message(STATUS "notcurses found β€” building nitro") add_executable(nitro nitro.cpp ) @@ -206,6 +249,12 @@ if(NC_FOUND) ggml ${NC_TARGET} # imported target carries include + lib paths ) + if(CURL_FOUND_INTERNAL) + target_link_libraries(nitro PRIVATE ${CURL_TARGET}) + target_compile_definitions(nitro PRIVATE NITRO_HAVE_CURL=1) + else() + target_compile_definitions(nitro PRIVATE NITRO_HAVE_CURL=0) + endif() set_target_properties(nitro PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin ) @@ -213,28 +262,6 @@ else() message(STATUS "notcurses not found β€” skipping nitro (set -DNOTCURSES_DIR=... to enable)") endif() -# ----------------------------- -# RAG indexer -# ----------------------------- -add_executable(rag_index - rag_index.cpp -) - -target_include_directories(rag_index PRIVATE - ${LLAMA_DIR}/include - ${LLAMA_DIR}/ggml/include -) - -target_link_libraries(rag_index PRIVATE - llm - llama - ggml -) - -set_target_properties(rag_index PROPERTIES - RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin -) - # ----------------------------- # Header preparation for RAG indexer # ----------------------------- diff --git a/llama/nitro.cpp b/llama/nitro.cpp index 6e82149..cd63674 100644 --- a/llama/nitro.cpp +++ b/llama/nitro.cpp @@ -6,7 +6,7 @@ // g++ -std=c++20 -O2 nitro.cpp llama-sb.cpp \ // -I/path/to/llama.cpp/include \ // -L/path/to/llama.cpp/build/src \ -// -lllama -lggml -lnotcurses-core -lnotcurses \ +// -lllama -lggml -lnotcurses-core -lnotcurses -lcurl \ // -o nitro // // Usage: @@ -35,9 +35,9 @@ // TOOL:TIME // TOOL:RND // TOOL:PERMISSION +// TOOL:CURL // // Copyright (C) 2026 Chris Warren-Smith β€” GPLv2 or later - // ─── Standard library ──────────────────────────────────────────────────────── #include #include @@ -50,23 +50,20 @@ #include #include #include - +// ─── curl ───────────────────────────────────────────────────────────────────── +#include // ─── Integration layer (sole llama.cpp dependency for nitro) ───────────────── #include "llama-sb.h" #include "llama-sb-rag.h" - // ─── TUI ───────────────────────────────────────────────────────────────────── #include - namespace fs = std::filesystem; - // ═══════════════════════════════════════════════════════════════════════════ // Forward declarations // ═══════════════════════════════════════════════════════════════════════════ struct NitroConfig; struct TuiState; struct AgentState; - static std::string join_path(const std::string &a, const std::string &b); static std::string read_file(const std::string &path); static bool write_file(const std::string &path, const std::string &data); @@ -77,13 +74,154 @@ static std::string process_tool(const std::string &line, const std::string &san TuiState &tui); static std::string build_system_prompt(const std::vector &knowledge_files, const std::string &sandbox); +// ─── RAG indexing ───────────────────────────────────────────────────────────── +static constexpr int BATCH_SIZE = 512; + +struct Chunk { + std::string text; + std::string source; + std::string type; + std::vector embedding; +}; + +static bool json_get_string(const std::string &json, + const std::string &key, + std::string &out) { + std::string search = "\"" + key + "\":"; + size_t pos = json.find(search); + if (pos == std::string::npos) return false; + pos += search.size(); + while (pos < json.size() && json[pos] == ' ') ++pos; + if (pos >= json.size() || json[pos] != '"') return false; + ++pos; + out.clear(); + while (pos < json.size()) { + char c = json[pos++]; + if (c == '\\' && pos < json.size()) { + char e = json[pos++]; + switch (e) { + case 'n': out += '\n'; break; + case 't': out += '\t'; break; + case '"': out += '"'; break; + case '\\': out += '\\'; break; + default: out += e; break; + } + } else if (c == '"') { + break; + } else { + out += c; + } + } + return true; +} + +static bool save_db(const std::string &path, + const std::vector &chunks, + int embed_dim) { + std::ofstream f(path, std::ios::binary); + if (!f) { + std::fprintf(stderr, "cannot open for write: %s\n\n", path); + return false; + } + auto write32 = [&](uint32_t v) { f.write((char*)&v, 4); }; + auto write16 = [&](uint16_t v) { f.write((char*)&v, 2); }; + auto write8 = [&](uint8_t v) { f.write((char*)&v, 1); }; + auto writestr = [&](const std::string &s, size_t max_len) { + size_t len = std::min(s.size(), max_len); + f.write(s.c_str(), (std::streamsize)len); + }; + write32(0x52414744); + write32(2); + write32((uint32_t)chunks.size()); + write32((uint32_t)embed_dim); + for (const Chunk &c : chunks) { + write32((uint32_t)c.text.size()); + f.write(c.text.c_str(), (std::streamsize)c.text.size()); + uint16_t src_len = (uint16_t)std::min(c.source.size(), (size_t)65535); + write16(src_len); + writestr(c.source, src_len); + uint8_t type_len = (uint8_t)std::min(c.type.size(), (size_t)255); + write8(type_len); + writestr(c.type, type_len); + f.write((char*)c.embedding.data(), + (std::streamsize)(embed_dim * sizeof(float))); + } + return f.good(); +} // ═══════════════════════════════════════════════════════════════════════════ -// Config (mirrors the SB agent constants) +// InputHistory β€” up/down arrow navigation through submitted inputs // ═══════════════════════════════════════════════════════════════════════════ +class InputHistory { + public: + explicit InputHistory() = default; + ~InputHistory() = default; + InputHistory(const InputHistory &) = delete; + InputHistory &operator=(const InputHistory &) = delete; + + /** + * @brief Adds a new command string to the history stack. + * Resets navigation index upon adding a new item. + * Deduplicates consecutive identical entries. + */ + void push(const std::string &input) { + if (input.empty()) return; + if (!history_stack.empty() && history_stack.back() == input) { + // Don't push duplicate of last entry; just reset nav position. + current_index = (int)history_stack.size(); + return; + } + history_stack.push_back(input); + current_index = (int)history_stack.size(); + } + + /** + * @brief Navigates to an earlier entry. + * @param out Set to the selected entry on success. + * @return true if an item was successfully retrieved. + */ + bool up(std::string &out) { + if (history_stack.empty() || current_index <= 0) return false; + --current_index; + out = history_stack[current_index]; + return true; + } + + /** + * @brief Navigates to a later entry, or clears when past the newest. + * @param out Set to the selected entry, or cleared if past the end. + * @return true if a history entry was retrieved (false means "clear input"). + */ + bool down(std::string &out) { + if (history_stack.empty()) return false; + ++current_index; + if (current_index >= (int)history_stack.size()) { + current_index = (int)history_stack.size(); + out.clear(); + return false; // signal: restore blank input + } + out = history_stack[current_index]; + return true; + } + + /** Reset navigation position without modifying the stack. */ + void reset_nav() { + current_index = (int)history_stack.size(); + } + + private: + std::vector history_stack; + int current_index = 0; +}; + +// ═══════════════════════════════════════════════════════════════════════════ +// Settings persistence (~/.config/nitro.settings.json) +// ═══════════════════════════════════════════════════════════════════════════ +// A minimal hand-rolled JSON reader/writer for the flat key-value settings +// we care about. We deliberately avoid a full JSON library dependency. struct NitroConfig { - std::string model_path; // empty = no model yet; set via -m/--model or /model + std::string model_path; std::string embed_path; std::string sandbox; int n_ctx = 65536; @@ -101,6 +239,135 @@ struct NitroConfig { int rag_top_k = 5; }; +// Returns the canonical settings path: ~/.config/nitro.settings.json +static std::string settings_path() { + const char *home = getenv("HOME"); + std::string base = home ? std::string(home) : "."; + return base + "/.config/nitro.settings.json"; +} + +// Tiny helper: extract a quoted string value from flat JSON for a known key. +static bool settings_get_str(const std::string &json, + const std::string &key, + std::string &out) { + return json_get_string(json, key, out); +} + +// Tiny helper: extract an integer value from flat JSON. +static bool settings_get_int(const std::string &json, + const std::string &key, + int &out) { + std::string search = "\"" + key + "\":"; + size_t pos = json.find(search); + if (pos == std::string::npos) return false; + pos += search.size(); + while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t')) ++pos; + if (pos >= json.size()) return false; + // read digits (and optional leading minus) + size_t start = pos; + if (json[pos] == '-') ++pos; + while (pos < json.size() && std::isdigit((unsigned char)json[pos])) ++pos; + if (pos == start) return false; + out = std::stoi(json.substr(start, pos - start)); + return true; +} + +// Tiny helper: extract a float value from flat JSON. +static bool settings_get_float(const std::string &json, + const std::string &key, + float &out) { + std::string search = "\"" + key + "\":"; + size_t pos = json.find(search); + if (pos == std::string::npos) return false; + pos += search.size(); + while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t')) ++pos; + if (pos >= json.size()) return false; + size_t start = pos; + if (json[pos] == '-') ++pos; + while (pos < json.size() && (std::isdigit((unsigned char)json[pos]) || json[pos] == '.')) ++pos; + if (pos == start) return false; + out = std::stof(json.substr(start, pos - start)); + return true; +} + +// Load settings from disk into cfg. Fields present in the file overwrite +// the defaults already in cfg; fields absent are left at their defaults. +// Silently succeeds if the file doesn't exist yet. +static void load_settings(NitroConfig &cfg) { + std::string path = settings_path(); + std::ifstream f(path); + if (!f) return; // no file β†’ use defaults + std::ostringstream oss; oss << f.rdbuf(); + std::string json = oss.str(); + + // String fields + settings_get_str(json, "model_path", cfg.model_path); + settings_get_str(json, "embed_path", cfg.embed_path); + settings_get_str(json, "sandbox", cfg.sandbox); + + // Integer fields + settings_get_int(json, "n_ctx", cfg.n_ctx); + settings_get_int(json, "n_batch", cfg.n_batch); + settings_get_int(json, "n_gpu_layers", cfg.n_gpu_layers); + settings_get_int(json, "n_max_tokens", cfg.n_max_tokens); + settings_get_int(json, "top_k", cfg.top_k); + settings_get_int(json, "penalty_last_n", cfg.penalty_last_n); + settings_get_int(json, "rag_top_k", cfg.rag_top_k); + + // Float fields + settings_get_float(json, "temperature", cfg.temperature); + settings_get_float(json, "top_p", cfg.top_p); + settings_get_float(json, "min_p", cfg.min_p); + settings_get_float(json, "penalty_repeat", cfg.penalty_repeat); +} + +// Escape a string for embedding in JSON. +static std::string json_escape(const std::string &s) { + std::string out; + out.reserve(s.size() + 4); + for (char c : s) { + switch (c) { + case '"': out += "\\\""; break; + case '\\': out += "\\\\"; break; + case '\n': out += "\\n"; break; + case '\t': out += "\\t"; break; + default: out += c; break; + } + } + return out; +} + +// Persist the current cfg to ~/.config/nitro.settings.json. +static bool save_settings(const NitroConfig &cfg) { + std::string path = settings_path(); + // Ensure ~/.config/ exists + fs::path dir = fs::path(path).parent_path(); + std::error_code ec; + fs::create_directories(dir, ec); + + std::ofstream f(path, std::ios::trunc); + if (!f) return false; + + f << "{\n"; + f << " \"model_path\": \"" << json_escape(cfg.model_path) << "\",\n"; + f << " \"embed_path\": \"" << json_escape(cfg.embed_path) << "\",\n"; + f << " \"sandbox\": \"" << json_escape(cfg.sandbox) << "\",\n"; + f << " \"n_ctx\": " << cfg.n_ctx << ",\n"; + f << " \"n_batch\": " << cfg.n_batch << ",\n"; + f << " \"n_gpu_layers\": " << cfg.n_gpu_layers << ",\n"; + f << " \"n_max_tokens\": " << cfg.n_max_tokens << ",\n"; + f << " \"temperature\": " << cfg.temperature << ",\n"; + f << " \"top_p\": " << cfg.top_p << ",\n"; + f << " \"min_p\": " << cfg.min_p << ",\n"; + f << " \"top_k\": " << cfg.top_k << ",\n"; + f << " \"penalty_repeat\": " << cfg.penalty_repeat << ",\n"; + f << " \"penalty_last_n\": " << cfg.penalty_last_n << ",\n"; + f << " \"rag_top_k\": " << cfg.rag_top_k << "\n"; + f << "}\n"; + + return f.good(); +} + // ═══════════════════════════════════════════════════════════════════════════ // Notcurses TUI // ═══════════════════════════════════════════════════════════════════════════ @@ -115,7 +382,6 @@ struct NitroConfig { // β”‚ ───────────────────────────────────── (separator) β”‚ // β”‚ ❯ input β”‚ // β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - struct TuiState { // ── notcurses handles ────────────────────────────────────────────── struct notcurses *nc = nullptr; @@ -123,77 +389,71 @@ struct TuiState { struct ncplane *header = nullptr; struct ncplane *chatpl = nullptr; struct ncplane *inputpl = nullptr; - // ── chat buffer ─────────────────────────────────────────────────── std::vector chat_lines; - int scroll_offset = 0; // lines scrolled up from bottom (0 = pinned) + int scroll_offset = 0; std::mutex lines_mutex; - // ── streaming accumulator ───────────────────────────────────────── - // Tokens arrive without newlines; we accumulate here and flush on \n. std::string token_acc; - // ── input ───────────────────────────────────────────────────────── std::string input_buf; size_t cursor_pos = 0; - - // ── status bar values (written by agent loop) ───────────────────── + // ── status bar values ───────────────────────────────────────────── std::string current_model = "none"; float tokens_per_sec = 0.0f; int kv_used = 0; int kv_total = 1; size_t vram_used = 0; size_t vram_total = 1; - int term_rows = 0; int term_cols = 0; - // ── thinking spinner ────────────────────────────────────────────── bool thinking = false; int spinner_frame = 0; + // ── input history ───────────────────────────────────────────────── + InputHistory history; // Advance spinner by one frame and redraw the header. void tick_spinner(); // Toggle thinking mode; redraws header immediately. void set_thinking(bool on); - // ── lifecycle ───────────────────────────────────────────────────── void init(); void destroy(); void resize(); - // ── draw ────────────────────────────────────────────────────────── void redraw_header(); void redraw_chat(); void redraw_input(); void redraw_all(); - // ── content helpers ─────────────────────────────────────────────── - // Append a complete line (wraps at terminal width, colour-coded by prefix). void append_line(const std::string &line); - // Feed a streaming token fragment; flushes complete lines on \n. void append_token(const std::string &token); - // Flush whatever is left in token_acc as a final line. void flush_token_acc(); - // ── interaction ─────────────────────────────────────────────────── - // Show a YES/NO confirm dialog in the input plane; writes "YES" or "NO". void confirm_dialog(const std::string &prompt, std::string &result); - // Blocking readline with cursor, arrow-key scrolling, basic editing. + // Blocking readline with history navigation, cursor, arrow-key scrolling. std::string readline_blocking(); + // Modal popup overlay while a long operation runs. + // Call show_modal_popup to display; dismiss_modal_popup to remove. + // The popup plane is stored in modal_plane; callers hold it as an opaque + // handle β€” or just use the paired helpers below. + struct ncplane *modal_plane = nullptr; + void show_modal_popup(const std::string &message); + void dismiss_modal_popup(); + // ── RAG folder picker popup ─────────────────────────────────────── + // Presents an interactive directory browser to let the user choose a + // folder (or file) to index. Returns the selected path, or empty string + // if the user cancelled. + std::string rag_folder_picker(const std::string &start_dir); }; - // ─── colour helpers ────────────────────────────────────────────────────── -// Our dark background colours (must match ncplane_set_base values in init). static constexpr uint32_t BG_CHAT_R = 18, BG_CHAT_G = 22, BG_CHAT_B = 30; static constexpr uint32_t BG_INP_R = 22, BG_INP_G = 28, BG_INP_B = 38; static constexpr uint32_t BG_HDR_R = 30, BG_HDR_G = 40, BG_HDR_B = 55; -// fg only (use only where bg is already set via ncplane_set_base) static inline uint64_t fg_rgb(uint32_t r, uint32_t g, uint32_t b) { return NCCHANNELS_INITIALIZER(r, g, b, 0, 0, 0); } -// fg + explicit bg β€” use this for all ncplane_set_channels calls so the -// background behind each glyph matches the plane's base colour exactly. static inline uint64_t chat_ch(uint32_t r, uint32_t g, uint32_t b) { return NCCHANNELS_INITIALIZER(r, g, b, BG_CHAT_R, BG_CHAT_G, BG_CHAT_B); } @@ -203,32 +463,22 @@ static inline uint64_t inp_ch(uint32_t r, uint32_t g, uint32_t b) { static inline uint64_t hdr_ch(uint32_t r, uint32_t g, uint32_t b) { return NCCHANNELS_INITIALIZER(r, g, b, BG_HDR_R, BG_HDR_G, BG_HDR_B); } - // ─── TuiState::init ────────────────────────────────────────────────────── - void TuiState::init() { notcurses_options opts{}; opts.flags = NCOPTION_SUPPRESS_BANNERS; nc = notcurses_init(&opts, nullptr); if (!nc) { std::fputs("notcurses_init failed\n", stderr); std::exit(1); } - stdpl = notcurses_stdplane(nc); notcurses_term_dim_yx(nc, (unsigned *)&term_rows, (unsigned *)&term_cols); - - // Fill the entire terminal with our dark background before creating - // child planes β€” eliminates the "terminal colour showing through" artefact. uint64_t bg = NCCHANNELS_INITIALIZER(BG_CHAT_R, BG_CHAT_G, BG_CHAT_B, BG_CHAT_R, BG_CHAT_G, BG_CHAT_B); ncplane_set_base(stdpl, " ", 0, bg); ncplane_erase(stdpl); - - // Header: row 0 ncplane_options hopt{}; hopt.y = 0; hopt.x = 0; hopt.rows = 1; hopt.cols = (unsigned)term_cols; header = ncplane_create(stdpl, &hopt); - - // Chat pane: rows 1 … term_rows-3 int chat_rows = std::max(1, term_rows - 3); ncplane_options copt{}; copt.y = 1; copt.x = 0; @@ -237,8 +487,6 @@ void TuiState::init() { ncplane_set_base(chatpl, " ", 0, NCCHANNELS_INITIALIZER(BG_CHAT_R, BG_CHAT_G, BG_CHAT_B, BG_CHAT_R, BG_CHAT_G, BG_CHAT_B)); - - // Input pane: last 2 rows ncplane_options iopt{}; iopt.y = term_rows - 2; iopt.x = 0; iopt.rows = 2; iopt.cols = (unsigned)term_cols; @@ -246,7 +494,6 @@ void TuiState::init() { ncplane_set_base(inputpl, " ", 0, NCCHANNELS_INITIALIZER(BG_INP_R, BG_INP_G, BG_INP_B, BG_INP_R, BG_INP_G, BG_INP_B)); - redraw_all(); } @@ -265,27 +512,21 @@ void TuiState::resize() { } // ─── TuiState::redraw_* ────────────────────────────────────────────────── - void TuiState::redraw_header() { ncplane_erase(header); ncplane_set_base(header, " ", 0, NCCHANNELS_INITIALIZER(BG_HDR_R, BG_HDR_G, BG_HDR_B, BG_HDR_R, BG_HDR_G, BG_HDR_B)); - float kv_pct = kv_total > 0 ? 100.f * (float)kv_used / (float)kv_total : 0.f; float vram_pct = vram_total > 0 ? 100.f * (float)vram_used / (float)vram_total : 0.f; - - // Spinner: braille dots rotate smoothly, clearly visible on the header static const char *const SPIN[] = { "β£Ύ","β£½","β£»","β’Ώ","β‘Ώ","⣟","β£―","β£·" }; const char *spin_str = thinking ? SPIN[spinner_frame % 8] : " "; - char buf[512]; int n = std::snprintf(buf, sizeof(buf), " ✦ NITRO β”‚ %-32s β”‚ %5.1f tok/s β”‚ KV %4.1f%% VRAM %4.1f%% %s", current_model.c_str(), (double)tokens_per_sec, (double)kv_pct, (double)vram_pct, spin_str); if (n > term_cols) buf[term_cols] = '\0'; - ncplane_set_channels(header, hdr_ch(130, 220, 200)); ncplane_putstr_yx(header, 0, 0, buf); } @@ -294,16 +535,13 @@ void TuiState::redraw_chat() { ncplane_erase(chatpl); unsigned rows, cols; ncplane_dim_yx(chatpl, &rows, &cols); - std::lock_guard lk(lines_mutex); int total = (int)chat_lines.size(); int visible = (int)rows; int start = std::max(0, total - visible - scroll_offset); int end = std::min(total, start + visible); - for (int i = start, row = 0; i < end; ++i, ++row) { const std::string &line = chat_lines[i]; - uint64_t ch; if (line.rfind("You: ", 0) == 0) ch = chat_ch(100, 200, 255); else if (line.rfind("Nitro: ", 0) == 0) ch = chat_ch(180, 255, 180); @@ -311,7 +549,6 @@ void TuiState::redraw_chat() { else if (line.rfind("[err]", 0) == 0) ch = chat_ch(255, 80, 80); else if (line.rfind("[sys]", 0) == 0) ch = chat_ch(140, 140, 200); else ch = chat_ch(210, 210, 210); - ncplane_set_channels(chatpl, ch); std::string display = line.size() > cols ? line.substr(0, cols) : line; ncplane_putstr_yx(chatpl, row, 0, display.c_str()); @@ -320,50 +557,34 @@ void TuiState::redraw_chat() { void TuiState::redraw_input() { ncplane_erase(inputpl); - - // Separator ncplane_set_channels(inputpl, inp_ch(80, 120, 160)); - std::string sep(term_cols, '─'); + std::string sep(term_cols, '-'); ncplane_putstr_yx(inputpl, 0, 0, sep.c_str()); - - // Prompt const std::string prompt = " ❯ "; const int prompt_cols = 4; ncplane_set_channels(inputpl, inp_ch(100, 210, 255)); ncplane_putstr_yx(inputpl, 1, 0, prompt.c_str()); - - // Buffer β€” split at cursor so we can render the cursor cell distinctly int max_w = std::max(0, term_cols - prompt_cols - 1); - - // Viewport: if buffer is wider than the available space, show the tail std::string visible = input_buf; int view_offset = 0; if ((int)visible.size() > max_w && max_w > 0) { view_offset = (int)visible.size() - max_w; visible = visible.substr(view_offset); } - - // Text before cursor int cur_in_view = std::max(0, (int)cursor_pos - view_offset); cur_in_view = std::min(cur_in_view, (int)visible.size()); - std::string before = visible.substr(0, cur_in_view); std::string after = cur_in_view < (int)visible.size() ? visible.substr(cur_in_view + 1) : ""; - char cursor_ch = cur_in_view < (int)visible.size() + char cursor_ch_val = cur_in_view < (int)visible.size() ? visible[cur_in_view] : ' '; - ncplane_set_channels(inputpl, inp_ch(230, 230, 230)); ncplane_putstr_yx(inputpl, 1, prompt_cols, before.c_str()); - - // Cursor cell: bright bg, dark text β€” stands out against the input bg int cx = prompt_cols + cur_in_view; ncplane_set_channels(inputpl, NCCHANNELS_INITIALIZER(BG_INP_R, BG_INP_G, BG_INP_B, 180, 230, 255)); - char cbuf[2] = { cursor_ch, '\0' }; + char cbuf[2] = { cursor_ch_val, '\0' }; ncplane_putstr_yx(inputpl, 1, cx, cbuf); - - // Text after cursor ncplane_set_channels(inputpl, inp_ch(230, 230, 230)); if (!after.empty()) ncplane_putstr_yx(inputpl, 1, cx + 1, after.c_str()); @@ -390,7 +611,6 @@ void TuiState::set_thinking(bool on) { } // ─── TuiState content helpers ───────────────────────────────────────────── - void TuiState::append_line(const std::string &line) { std::lock_guard lk(lines_mutex); int w = std::max(1, term_cols - 1); @@ -423,15 +643,279 @@ void TuiState::flush_token_acc() { } } -// ─── TuiState::confirm_dialog ───────────────────────────────────────────── +// ─── TuiState::show_modal_popup / dismiss_modal_popup ───────────────────── +// Creates a centred floating plane with a border and a status message. +// The popup sits above all other planes and blocks until explicitly dismissed. +void TuiState::show_modal_popup(const std::string &message) { + // Dismiss any previous popup first. + dismiss_modal_popup(); + + // Clamp popup size to terminal. + int popup_w = std::min((int)message.size() + 8, term_cols - 4); + popup_w = std::max(popup_w, 20); + int popup_h = 5; + int py = std::max(0, (term_rows - popup_h) / 2); + int px = std::max(0, (term_cols - popup_w) / 2); + + ncplane_options opts{}; + opts.y = py; opts.x = px; + opts.rows = (unsigned)popup_h; + opts.cols = (unsigned)popup_w; + modal_plane = ncplane_create(stdpl, &opts); + if (!modal_plane) return; + + // Background: deep navy. + static constexpr uint32_t PBG_R = 20, PBG_G = 28, PBG_B = 50; + ncplane_set_base(modal_plane, " ", 0, + NCCHANNELS_INITIALIZER(PBG_R, PBG_G, PBG_B, PBG_R, PBG_G, PBG_B)); + ncplane_erase(modal_plane); + + // Border β€” bright cyan. + uint64_t border_ch = NCCHANNELS_INITIALIZER(80, 220, 255, PBG_R, PBG_G, PBG_B); + ncplane_set_channels(modal_plane, border_ch); + + // Draw corners and edges manually so we don't require nccell border helpers. + // Top row + ncplane_putstr_yx(modal_plane, 0, 0, "β•”"); + for (int c = 1; c < popup_w - 1; ++c) + ncplane_putstr_yx(modal_plane, 0, c, "═"); + ncplane_putstr_yx(modal_plane, 0, popup_w - 1, "β•—"); + // Middle rows + for (int r = 1; r < popup_h - 1; ++r) { + ncplane_putstr_yx(modal_plane, r, 0, "β•‘"); + ncplane_putstr_yx(modal_plane, r, popup_w - 1, "β•‘"); + } + // Bottom row + ncplane_putstr_yx(modal_plane, popup_h - 1, 0, "β•š"); + for (int c = 1; c < popup_w - 1; ++c) + ncplane_putstr_yx(modal_plane, popup_h - 1, c, "═"); + ncplane_putstr_yx(modal_plane, popup_h - 1, popup_w - 1, "╝"); + + // Title bar. + uint64_t title_ch = NCCHANNELS_INITIALIZER(255, 220, 80, PBG_R, PBG_G, PBG_B); + ncplane_set_channels(modal_plane, title_ch); + ncplane_putstr_yx(modal_plane, 1, 2, "⏳ Loading…"); + + // Message. + uint64_t msg_ch = NCCHANNELS_INITIALIZER(200, 200, 200, PBG_R, PBG_G, PBG_B); + ncplane_set_channels(modal_plane, msg_ch); + // Truncate message to fit inside border. + int max_msg = popup_w - 4; + std::string display = message.size() > (size_t)max_msg + ? message.substr(0, max_msg) + : message; + ncplane_putstr_yx(modal_plane, 2, 2, display.c_str()); + + notcurses_render(nc); +} + +void TuiState::dismiss_modal_popup() { + if (modal_plane) { + ncplane_destroy(modal_plane); + modal_plane = nullptr; + notcurses_render(nc); + } +} + +// ─── TuiState::rag_folder_picker ────────────────────────────────────────── +// Interactive directory/file browser popup. +// Keyboard: ↑/↓ navigate, Enter select/descend, Backspace go up, +// 's' select current dir for indexing, Esc cancel. +// Returns the chosen path or "" on cancel. +std::string TuiState::rag_folder_picker(const std::string &start_dir) { + std::string current_dir = start_dir; + { + std::error_code ec; + auto canon = fs::canonical(start_dir, ec); + if (!ec) current_dir = canon.string(); + } + + // Build an entry list for the current directory. + auto load_entries = [](const std::string &dir, + std::vector &entries) { + entries.clear(); + std::error_code ec; + // Add ".." for going up (except at fs root). + if (fs::path(dir).has_parent_path() && fs::path(dir) != fs::path(dir).root_path()) + entries.push_back(".."); + // Dirs first, then files. + std::vector dirs, files; + for (const auto &e : fs::directory_iterator(dir, ec)) { + if (ec) break; + std::string name = e.path().filename().string(); + if (name.empty() || name[0] == '.') continue; + if (e.is_directory()) dirs.push_back(name); + else files.push_back(name); + } + std::sort(dirs.begin(), dirs.end()); + std::sort(files.begin(), files.end()); + for (auto &d : dirs) entries.push_back(d + "/"); + for (auto &f : files) entries.push_back(f); + }; + + std::vector entries; + int selected = 0; + int scroll = 0; + + // Popup dimensions. + static constexpr int PW = 60; + static constexpr int PH = 20; + int py = std::max(0, (term_rows - PH) / 2); + int px = std::max(0, (term_cols - PW) / 2); + + ncplane_options opts{}; + opts.y = py; opts.x = px; + opts.rows = (unsigned)PH; opts.cols = (unsigned)PW; + struct ncplane *picker = ncplane_create(stdpl, &opts); + if (!picker) return ""; + + static constexpr uint32_t PBG_R = 18, PBG_G = 24, PBG_B = 40; + ncplane_set_base(picker, " ", 0, + NCCHANNELS_INITIALIZER(PBG_R, PBG_G, PBG_B, PBG_R, PBG_G, PBG_B)); + + auto draw_picker = [&]() { + ncplane_erase(picker); + uint64_t border_ch = NCCHANNELS_INITIALIZER(100, 180, 255, PBG_R, PBG_G, PBG_B); + ncplane_set_channels(picker, border_ch); + // Border + ncplane_putstr_yx(picker, 0, 0, "β•”"); + for (int c = 1; c < PW - 1; ++c) ncplane_putstr_yx(picker, 0, c, "═"); + ncplane_putstr_yx(picker, 0, PW - 1, "β•—"); + for (int r = 1; r < PH - 1; ++r) { + ncplane_putstr_yx(picker, r, 0, "β•‘"); + ncplane_putstr_yx(picker, r, PW - 1, "β•‘"); + } + ncplane_putstr_yx(picker, PH - 1, 0, "β•š"); + for (int c = 1; c < PW - 1; ++c) ncplane_putstr_yx(picker, PH - 1, c, "═"); + ncplane_putstr_yx(picker, PH - 1, PW - 1, "╝"); + + // Title + ncplane_set_channels(picker, + NCCHANNELS_INITIALIZER(255, 220, 80, PBG_R, PBG_G, PBG_B)); + ncplane_putstr_yx(picker, 0, 2, " πŸ“‚ RAG Folder Picker "); + + // Current path (truncated to fit) + std::string path_display = current_dir; + if ((int)path_display.size() > PW - 4) + path_display = "…" + path_display.substr(path_display.size() - (PW - 5)); + ncplane_set_channels(picker, + NCCHANNELS_INITIALIZER(160, 200, 240, PBG_R, PBG_G, PBG_B)); + ncplane_putstr_yx(picker, 1, 2, path_display.c_str()); + + // Hint line + ncplane_set_channels(picker, + NCCHANNELS_INITIALIZER(120, 120, 160, PBG_R, PBG_G, PBG_B)); + ncplane_putstr_yx(picker, PH - 2, 2, + "↑↓ navigate Enter open s=select dir Esc cancel"); + + // Entry list + int list_rows = PH - 5; // rows 2 … PH-4 available + // Clamp scroll so selected stays visible + if (selected < scroll) scroll = selected; + if (selected >= scroll + list_rows) scroll = selected - list_rows + 1; + + for (int i = 0; i < list_rows; ++i) { + int idx = scroll + i; + if (idx >= (int)entries.size()) break; + bool is_selected = (idx == selected); + bool is_dir = !entries[idx].empty() && entries[idx].back() == '/'; + uint32_t fr, fg, fb; + if (is_selected) { fr = 20; fg = 20; fb = 20; } + else if (is_dir) { fr = 120; fg = 200; fb = 255; } + else { fr = 200; fg = 200; fb = 200; } + uint32_t br = is_selected ? 100 : PBG_R; + uint32_t bg = is_selected ? 180 : PBG_G; + uint32_t bb = is_selected ? 255 : PBG_B; + ncplane_set_channels(picker, + NCCHANNELS_INITIALIZER(fr, fg, fb, br, bg, bb)); + // Pad entry to fill width + std::string label = (is_selected ? " β–Ά " : " ") + entries[idx]; + if ((int)label.size() > PW - 2) label = label.substr(0, PW - 2); + while ((int)label.size() < PW - 2) label += ' '; + ncplane_putstr_yx(picker, 2 + i, 1, label.c_str()); + } + notcurses_render(nc); + }; + + std::string result; + load_entries(current_dir, entries); + draw_picker(); + for (;;) { + ncinput ni{}; + notcurses_get_blocking(nc, &ni); + if (ni.id == NCKEY_ESC) { + break; // cancelled + } + if (ni.id == NCKEY_UP) { + if (selected > 0) --selected; + draw_picker(); + continue; + } + if (ni.id == NCKEY_DOWN) { + if (selected + 1 < (int)entries.size()) ++selected; + draw_picker(); + continue; + } + if (ni.id == 's' || ni.id == 'S') { + // Select current directory for RAG indexing. + result = current_dir; + break; + } + if (ni.id == NCKEY_BACKSPACE || ni.id == 127) { + // Go up one level. + fs::path p(current_dir); + if (p.has_parent_path() && p != p.root_path()) { + current_dir = p.parent_path().string(); + load_entries(current_dir, entries); + selected = 0; scroll = 0; + draw_picker(); + } + continue; + } + if (ni.id == NCKEY_ENTER || ni.id == '\r' || ni.id == '\n') { + if (entries.empty()) continue; + const std::string &entry = entries[selected]; + if (entry == "..") { + fs::path p(current_dir); + if (p.has_parent_path() && p != p.root_path()) { + current_dir = p.parent_path().string(); + load_entries(current_dir, entries); + selected = 0; scroll = 0; + draw_picker(); + } + } else if (!entry.empty() && entry.back() == '/') { + // Descend into directory. + current_dir = current_dir + "/" + entry.substr(0, entry.size() - 1); + { + std::error_code ec; + auto canon = fs::canonical(current_dir, ec); + if (!ec) current_dir = canon.string(); + } + load_entries(current_dir, entries); + selected = 0; scroll = 0; + draw_picker(); + } else { + // Select a specific file. + result = current_dir + "/" + entry; + break; + } + continue; + } + } + + ncplane_destroy(picker); + notcurses_render(nc); + return result; +} + +// ─── TuiState::confirm_dialog ───────────────────────────────────────────── void TuiState::confirm_dialog(const std::string &prompt, std::string &result) { ncplane_erase(inputpl); ncplane_set_channels(inputpl, inp_ch(255, 200, 80)); std::string msg = " " + prompt + " [y/n] ❯ "; ncplane_putstr_yx(inputpl, 1, 0, msg.c_str()); notcurses_render(nc); - std::string answer; for (;;) { ncinput ni{}; @@ -439,13 +923,11 @@ void TuiState::confirm_dialog(const std::string &prompt, std::string &result) { if (ni.id == NCKEY_ENTER || ni.id == '\r' || ni.id == '\n') break; if (ni.id == NCKEY_BACKSPACE && !answer.empty()) { answer.pop_back(); } else if (ni.id >= 32 && ni.id < 127) { answer += (char)ni.id; } - ncplane_erase(inputpl); ncplane_set_channels(inputpl, inp_ch(255, 200, 80)); ncplane_putstr_yx(inputpl, 1, 0, (msg + answer).c_str()); notcurses_render(nc); } - std::string lo = answer; std::transform(lo.begin(), lo.end(), lo.begin(), ::tolower); result = (lo == "y" || lo == "yes" || lo == "sure" || lo == "k") ? "YES" : "NO"; @@ -454,23 +936,83 @@ void TuiState::confirm_dialog(const std::string &prompt, std::string &result) { } // ─── TuiState::readline_blocking ────────────────────────────────────────── - +// Integrates InputHistory: Up/Down arrows navigate the history stack. +// On submit the entry is pushed to history, and nav is reset. std::string TuiState::readline_blocking() { input_buf.clear(); cursor_pos = 0; + history.reset_nav(); redraw_input(); notcurses_render(nc); + // Temporary saved draft so Down from history restores the user's current text. + std::string draft; + for (;;) { ncinput ni{}; notcurses_get_blocking(nc, &ni); if (ni.id == NCKEY_ENTER || ni.id == '\r' || ni.id == '\n') { std::string result = input_buf; - input_buf.clear(); cursor_pos = 0; - redraw_input(); notcurses_render(nc); + if (!result.empty()) { + history.push(result); + } + input_buf.clear(); + cursor_pos = 0; + redraw_input(); + notcurses_render(nc); return result; } + + if (ni.id == NCKEY_UP) { + // Entering history from a fresh prompt: save current text as draft. + std::string hist_entry; + if (history.up(hist_entry)) { + if (input_buf.size() > 0 && hist_entry != input_buf) { + // Only save draft when we first leave the bottom of history. + // (history.reset_nav was called on entry so the first Up call + // always comes from the "new input" position.) + draft = input_buf; + } + input_buf = hist_entry; + cursor_pos = input_buf.size(); + } + redraw_input(); + notcurses_render(nc); + continue; + } + + if (ni.id == NCKEY_DOWN) { + std::string hist_entry; + bool got = history.down(hist_entry); + if (got) { + input_buf = hist_entry; + cursor_pos = input_buf.size(); + } else { + // Past the newest entry β†’ restore draft. + input_buf = draft; + cursor_pos = input_buf.size(); + draft.clear(); + } + redraw_input(); + notcurses_render(nc); + continue; + } + + // Scroll the chat pane β€” not the input history. + if (ni.id == NCKEY_PGUP) { + scroll_offset += std::max(1, term_rows - 4); + redraw_chat(); + notcurses_render(nc); + continue; + } + if (ni.id == NCKEY_PGDOWN) { + scroll_offset = std::max(0, scroll_offset - std::max(1, term_rows - 4)); + redraw_chat(); + notcurses_render(nc); + continue; + } + if (ni.id == NCKEY_BACKSPACE || ni.id == 127) { if (cursor_pos > 0) { input_buf.erase(cursor_pos - 1, 1); --cursor_pos; } } else if (ni.id == NCKEY_LEFT) { @@ -481,79 +1023,46 @@ std::string TuiState::readline_blocking() { cursor_pos = 0; } else if (ni.id == NCKEY_END) { cursor_pos = input_buf.size(); - } else if (ni.id == NCKEY_UP) { - ++scroll_offset; redraw_chat(); notcurses_render(nc); continue; - } else if (ni.id == NCKEY_DOWN) { - if (scroll_offset > 0) --scroll_offset; - redraw_chat(); notcurses_render(nc); continue; + } else if (ni.id == NCKEY_DEL) { + if (cursor_pos < input_buf.size()) input_buf.erase(cursor_pos, 1); } else if (ni.id >= 32 && ni.id < 0xD800) { + // Any printable character β€” entering new text clears the nav draft + // so that Down won't resurrect a stale saved buffer. + draft.clear(); + history.reset_nav(); input_buf.insert(cursor_pos, 1, (char)ni.id); ++cursor_pos; } + redraw_input(); notcurses_render(nc); } } // ═══════════════════════════════════════════════════════════════════════════ -// AgentState β€” thin owner of Llama + LlamaIter + optional RAG objects. -// -// Design: -// β€’ Llama is created once; settings are applied before load_model(). -// β€’ iter is an std::optional so we can construct it lazily (LlamaIter has -// no default-construct-then-assign path once _has_next is false; we just -// move a fresh one in via add_message()). -// β€’ reset_conversation() calls llama.reset() which clears the KV cache, -// then re-injects the system prompt as the first message of the new turn. -// β€’ run_turn() mirrors the SB main() loop exactly: -// while iter.has_next β†’ next() β†’ accumulate line β†’ on TOOL: dispatch +// AgentState // ═══════════════════════════════════════════════════════════════════════════ - struct AgentState { Llama llama; - - // iter is valid (has_next may be false) after the first add_message call. - // We use a pointer so it can be replaced by move. std::unique_ptr iter; - - // Separate Llama instance for embeddings (optional) std::unique_ptr embed_llama; - - // RAG objects std::unique_ptr rag_db; std::unique_ptr rag_session; - bool model_loaded = false; std::string system_prompt; - // ── setup ───────────────────────────────────────────────────────── bool setup_model(const NitroConfig &cfg, TuiState &tui); bool setup_embed(const std::string &path, TuiState &tui); void apply_generation_params(const NitroConfig &cfg); - - // ── conversation management ─────────────────────────────────────── - // Injects the system prompt as a fresh first turn. - // Call after setup_model() or whenever /clear is issued. void reset_conversation(const std::string &sysprompt, TuiState &tui); - - // ── generation ──────────────────────────────────────────────────── - // Returns false on fatal error. bool run_turn(const std::string &user_message, const NitroConfig &cfg, TuiState &tui); - - // ── RAG ─────────────────────────────────────────────────────────── bool rag_index(const std::string &path, TuiState &tui); - - // ── status ──────────────────────────────────────────────────────── std::string memory_info_text(); - - // Compute tok/s from iter (matches SB iter.tokens_sec() idiom) float tokens_per_sec() const; }; -// ─── AgentState::setup_model ────────────────────────────────────────────── - void AgentState::apply_generation_params(const NitroConfig &cfg) { llama.add_stop("<|turn|>"); llama.add_stop("<|im_end|>"); @@ -567,29 +1076,32 @@ void AgentState::apply_generation_params(const NitroConfig &cfg) { llama.set_log_level(cfg.log_level); } +// ─── AgentState::setup_model ────────────────────────────────────────────── +// Shows a modal loading popup while the model loads. bool AgentState::setup_model(const NitroConfig &cfg, TuiState &tui) { if (cfg.model_path.empty()) { tui.append_line("[sys] No model loaded. Use /model to load a GGUF."); tui.redraw_all(); return false; } + // Show a modal popup so the user knows loading is in progress. + std::string model_name = fs::path(cfg.model_path).filename().string(); + tui.show_modal_popup("Loading " + model_name); - // reset() clears any previous KV state cleanly llama.reset(); apply_generation_params(cfg); - if (!llama.load_model(cfg.model_path, cfg.n_ctx, cfg.n_batch, cfg.n_gpu_layers, cfg.log_level)) { + tui.dismiss_modal_popup(); tui.append_line(std::string("[err] ") + llama.last_error()); tui.redraw_all(); return false; } + tui.dismiss_modal_popup(); model_loaded = true; - tui.current_model = fs::path(cfg.model_path).filename().string(); + tui.current_model = model_name; tui.append_line("[sys] Model ready: " + tui.current_model); - - // Show memory advice (mirrors SB: print GREEN + mem.advice) LlamaMemoryInfo mem = llama.memory_info(); tui.append_line("[sys] " + mem.advice); tui.kv_used = mem.kv_used; @@ -601,15 +1113,17 @@ bool AgentState::setup_model(const NitroConfig &cfg, TuiState &tui) { } bool AgentState::setup_embed(const std::string &path, TuiState &tui) { - tui.append_line("[sys] Loading embedding model: " + path); + tui.show_modal_popup("Loading embedding model: " + fs::path(path).filename().string()); tui.redraw_all(); embed_llama = std::make_unique(); if (!embed_llama->load_embedding_model(path)) { + tui.dismiss_modal_popup(); tui.append_line(std::string("[err] ") + embed_llama->last_error()); tui.redraw_all(); embed_llama.reset(); return false; } + tui.dismiss_modal_popup(); rag_db = std::make_unique(); rag_session = std::make_unique(); tui.append_line("[sys] Embedding model ready."); @@ -617,15 +1131,10 @@ bool AgentState::setup_embed(const std::string &path, TuiState &tui) { return true; } -// ─── AgentState::reset_conversation ────────────────────────────────────── -// Mirrors the SB pattern: -// local iter = llama.add_message("system", initialize_agent()) - void AgentState::reset_conversation(const std::string &sysprompt, TuiState &tui) { system_prompt = sysprompt; - llama.reset(); // clears KV cache + sampler state - apply_generation_params(NitroConfig{}); // re-apply stops / params after reset - + llama.reset(); + apply_generation_params(NitroConfig{}); iter = std::make_unique(); if (!llama.add_message(*iter, "system", system_prompt)) { tui.append_line(std::string("[err] System prompt injection: ") + llama.last_error()); @@ -633,11 +1142,6 @@ void AgentState::reset_conversation(const std::string &sysprompt, TuiState &tui) } } -// ─── AgentState::tokens_per_sec ────────────────────────────────────────── -// LlamaIter stores _t_start and _tokens_generated; we replicate the SB -// iter.tokens_sec() calculation here since LlamaIter doesn't expose it -// as a method in the public header. - float AgentState::tokens_per_sec() const { if (!iter) return 0.0f; auto now = std::chrono::high_resolution_clock::now(); @@ -646,8 +1150,6 @@ float AgentState::tokens_per_sec() const { return (float)(iter->_tokens_generated / elapsed); } -// ─── AgentState::memory_info_text ──────────────────────────────────────── - std::string AgentState::memory_info_text() { if (!model_loaded) return "No model loaded."; LlamaMemoryInfo m = llama.memory_info(); @@ -664,15 +1166,12 @@ std::string AgentState::memory_info_text() { return oss.str(); } -// ─── AgentState::rag_index ─────────────────────────────────────────────── - bool AgentState::rag_index(const std::string &path, TuiState &tui) { if (!embed_llama || !rag_db) { tui.append_line("[err] Load an embedding model first: /embed "); tui.redraw_all(); return false; } - auto index_one = [&](const std::string &filepath) { tui.append_line("[sys] indexing: " + filepath); tui.redraw_all(); @@ -681,7 +1180,6 @@ bool AgentState::rag_index(const std::string &path, TuiState &tui) { tui.redraw_all(); } }; - fs::path rp(path); std::error_code ec; if (fs::is_directory(rp, ec)) { @@ -695,20 +1193,8 @@ bool AgentState::rag_index(const std::string &path, TuiState &tui) { } // ═══════════════════════════════════════════════════════════════════════════ -// Agent turn β€” mirrors SB main() loop -// -// SB pattern (condensed): -// while iter.has_next() -// buffer += iter.next() -// if newline in buffer: -// line = left side of buffer -// if TOOL: β†’ line += buffer + iter.all() -// iter = llama.add_message("tool", process_tool(line)) -// else print line -// if remaining buffer is TOOL: β†’ process it -// else flush remaining buffer +// Agent turn // ═══════════════════════════════════════════════════════════════════════════ - bool AgentState::run_turn(const std::string &user_message, const NitroConfig &cfg, TuiState &tui) { @@ -717,9 +1203,6 @@ bool AgentState::run_turn(const std::string &user_message, tui.redraw_all(); return false; } - - // ── optional RAG context injection ─────────────────────────────── - // If we have a RAG session, prepend retrieved context to the user message. std::string effective_message = user_message; if (embed_llama && rag_db && rag_session) { std::string context = llama.rag_retrieve(*rag_db, user_message, @@ -728,68 +1211,51 @@ bool AgentState::run_turn(const std::string &user_message, effective_message = "Context:\n" + context + "\n\nUser: " + user_message; } } - - // ── inject user message ─────────────────────────────────────────── - // iter must already exist (reset_conversation initialises it with "system"). - // add_message("user", …) appends to the existing KV context. if (!iter) { tui.append_line("[err] Conversation not initialised (call /clear to reset)"); tui.redraw_all(); return false; } - if (!llama.add_message(*iter, "user", effective_message)) { tui.append_line(std::string("[err] add_message: ") + llama.last_error()); tui.redraw_all(); return false; } - - // ── label the assistant response in the chat pane ──────────────── tui.append_line("Nitro: "); tui.set_thinking(true); - // ── generation loop ─────────────────────────────────────────────── - bool in_think = false; - std::string buffer; // accumulates tokens until we see a newline + bool in_think = true; + std::string buffer; - // Scan buffer for think open/close tags anywhere in the text. - // Models use either or <|think|> depending on the template. auto update_think_state = [&](const std::string &text) { - // Opening tags if (text.find("") != std::string::npos || text.find("<|think|>") != std::string::npos) in_think = true; - // Closing tags β€” check after open so a single-line … ends correctly if (text.find("") != std::string::npos || text.find("") != std::string::npos) in_think = false; }; + auto remove_substr = [](std::string str, const std::string& toRemove) { + size_t pos = str.find(toRemove); + while (pos != std::string::npos) { + str.erase(pos, toRemove.length()); + pos = str.find(toRemove, pos); + } + return str; + }; + while (iter->_has_next) { std::string tok = llama.next(*iter); tui.tick_spinner(); - - // Update think state on every token so tags that arrive mid-buffer - // suppress display immediately rather than waiting for a newline. update_think_state(tok); - buffer += tok; - auto nl = buffer.find('\n'); if (nl != std::string::npos) { std::string text_line = buffer.substr(0, nl); buffer = buffer.substr(nl + 1); - - // Trim leading whitespace to detect TOOL: reliably std::string trimmed = text_line; trimmed.erase(0, trimmed.find_first_not_of(" \t")); - if (trimmed.substr(0, 5) == "TOOL:") { - // Collect remainder: rest of buffer + everything iter still has. - // For TOOL:WRITE the content may contain newlines, so we keep - // the raw text and only strip newlines from the op+arg1 prefix. std::string tail = buffer + llama.all(*iter); - - // Parse op and arg1 from trimmed (first two space-separated tokens) - // then treat everything after as the raw payload (preserving newlines). std::string op, arg1, payload; { auto s1 = trimmed.find(' '); @@ -809,73 +1275,57 @@ bool AgentState::run_turn(const std::string &user_message, op = trimmed; } } - - // Reconstruct the tool line. For ops that don't carry a file payload - // (LIST, EXISTS, READ, DATE, TIME, RND, PERMISSION, RUN) we still - // collapse newlines in payload so the single-line format is preserved. - // For TOOL:WRITE we keep newlines in the payload intact. std::string tool_line; if (op == "TOOL:WRITE") { tool_line = op + " " + arg1 + " " + payload; - // Trim only trailing whitespace - while (!tool_line.empty() && tool_line.back() == '\n') - tool_line.pop_back(); + while (!tool_line.empty() && tool_line.back() == '\n') tool_line.pop_back(); } else { tool_line = op; - if (!arg1.empty()) tool_line += " " + arg1; + if (!arg1.empty()) tool_line += " " + arg1; if (!payload.empty()) { std::string flat = payload; flat.erase(std::remove(flat.begin(), flat.end(), '\n'), flat.end()); - while (!flat.empty() && std::isspace((unsigned char)flat.back())) - flat.pop_back(); + while (!flat.empty() && std::isspace((unsigned char)flat.back())) flat.pop_back(); if (!flat.empty()) tool_line += " " + flat; } } - tui.append_line("[tool] " + op + " " + arg1 + (op == "TOOL:WRITE" ? " " : "")); tui.redraw_all(); - std::string result = process_tool(tool_line, cfg.sandbox, tui); - tui.append_line("[tool] β†’ " + result.substr(0, 200) + (result.size() > 200 ? "…" : "")); tui.redraw_all(); - if (!llama.add_message(*iter, "tool", result)) { tui.append_line(std::string("[err] tool result inject: ") + llama.last_error()); tui.redraw_all(); break; } buffer.clear(); - - } else { - // Normal output line β€” suppress if inside think block - if (!in_think) { - tui.append_token(text_line + "\n"); - } + } else if (!in_think) { + text_line = remove_substr(text_line, ""); + text_line = remove_substr(text_line, ""); + tui.append_token(text_line + "\n"); } } } - // ── flush remaining buffer ──────────────────────────────────────── if (!buffer.empty()) { std::string trimmed = buffer; trimmed.erase(0, trimmed.find_first_not_of(" \t")); - if (trimmed.substr(0, 5) == "TOOL:") { std::string result = process_tool(trimmed, cfg.sandbox, tui); tui.append_line("[tool] β†’ " + result.substr(0, 200)); tui.redraw_all(); llama.add_message(*iter, "tool", result); - } else { - if (!in_think) tui.append_token(buffer); + } else if (!in_think) { + tui.append_token(buffer); } } + tui.flush_token_acc(); tui.set_thinking(false); - // ── update status bar ───────────────────────────────────────────── tui.tokens_per_sec = tokens_per_sec(); LlamaMemoryInfo mem = llama.memory_info(); tui.kv_used = mem.kv_used; @@ -883,7 +1333,6 @@ bool AgentState::run_turn(const std::string &user_message, tui.vram_used = mem.vram_used; tui.vram_total = mem.vram_total; - // ── stat line (mirrors SB: "Tokens/sec: …") ────────────────────── char stat[128]; std::snprintf(stat, sizeof(stat), "[sys] %.1f tok/s (%d tokens) KV %.1f%%", (double)tui.tokens_per_sec, @@ -897,7 +1346,6 @@ bool AgentState::run_turn(const std::string &user_message, // ═══════════════════════════════════════════════════════════════════════════ // File-system helpers // ═══════════════════════════════════════════════════════════════════════════ - static std::string join_path(const std::string &a, const std::string &b) { if (b.empty()) return a; if (b[0] == '/') return b; @@ -969,13 +1417,60 @@ static std::string strip_code_fences(const std::string &filename, } // ═══════════════════════════════════════════════════════════════════════════ -// Tool dispatch (mirrors SB process_tool) +// TOOL:CURL β€” HTTP GET with libcurl, returns body text (capped at 32 KB). // ═══════════════════════════════════════════════════════════════════════════ +static size_t curl_write_cb(void *contents, size_t size, size_t nmemb, void *userp) { + std::string *buf = static_cast(userp); + size_t total = size * nmemb; + // Enforce a 32 KB cap to prevent flooding the context window. + static constexpr size_t MAX_BODY = 32 * 1024; + if (buf->size() < MAX_BODY) { + size_t room = MAX_BODY - buf->size(); + buf->append(static_cast(contents), std::min(total, room)); + } + return total; // Return full amount so curl doesn't abort. +} + +static std::string tool_curl(const std::string &url) { + if (url.empty()) return "ERROR: TOOL:CURL requires a URL argument"; + + CURL *curl = curl_easy_init(); + if (!curl) return "ERROR: curl_easy_init failed"; + + std::string body; + body.reserve(4096); + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_cb); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &body); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5L); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L); // 15-second timeout + curl_easy_setopt(curl, CURLOPT_USERAGENT, "nitro/1.0"); + // Accept compressed responses; curl will decompress automatically. + curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, ""); + + CURLcode res = curl_easy_perform(curl); + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + curl_easy_cleanup(curl); + + if (res != CURLE_OK) { + return std::string("ERROR: curl: ") + curl_easy_strerror(res); + } + if (http_code >= 400) { + return "ERROR: HTTP " + std::to_string(http_code) + " from " + url; + } + if (body.empty()) return "(empty response)"; + return body; +} +// ═══════════════════════════════════════════════════════════════════════════ +// Tool dispatch +// ═══════════════════════════════════════════════════════════════════════════ static std::string process_tool(const std::string &cmd, const std::string &sandbox, TuiState &tui) { - // Parse: OP [ARG1 [REST…]] std::string op, arg1, arg2; auto sp1 = cmd.find(' '); if (sp1 == std::string::npos) { @@ -983,7 +1478,6 @@ static std::string process_tool(const std::string &cmd, } else { op = cmd.substr(0, sp1); std::string rest = cmd.substr(sp1 + 1); - // ltrim rest.erase(0, rest.find_first_not_of(" \t")); auto sp2 = rest.find(' '); if (sp2 == std::string::npos) { @@ -994,7 +1488,6 @@ static std::string process_tool(const std::string &cmd, } } - // Resolve arg1 into an absolute path inside the sandbox auto resolve = [&](const std::string &p) -> std::string { if (p.empty() || p == ".") return sandbox; if (p.substr(0, 2) == "./") return join_path(sandbox, p.substr(2)); @@ -1042,6 +1535,10 @@ static std::string process_tool(const std::string &cmd, tui.confirm_dialog("Allow model to proceed?", result); return result; } + if (op == "TOOL:CURL") { + // arg1 holds the URL (no sandbox restriction β€” network, not filesystem). + return tool_curl(arg1); + } if (op == "TOOL:RUN") { std::string prog = resolve(arg1); if (!path_in_sandbox(sandbox, prog)) return "ERROR: path outside sandbox"; @@ -1055,14 +1552,12 @@ static std::string process_tool(const std::string &cmd, if (out.size() > 4096) out = out.substr(0, 4096) + "\n…(truncated)"; return out; } - return "ERROR: unknown tool: " + op; } // ═══════════════════════════════════════════════════════════════════════════ -// System prompt (mirrors SB initialize_agent) +// System prompt // ═══════════════════════════════════════════════════════════════════════════ - static std::string build_system_prompt(const std::vector &knowledge_files, const std::string &sandbox) { std::string p; @@ -1080,13 +1575,14 @@ static std::string build_system_prompt(const std::vector &knowledge " TOOL:DATE current date\n" " TOOL:TIME current time\n" " TOOL:RND random float\n" - " TOOL:PERMISSION ask user for explicit permission\n\n" + " TOOL:PERMISSION ask user for explicit permission\n" + " TOOL:CURL HTTP GET; returns response body (max 32 KB)\n\n" "Rules:\n" "- Never access files outside the sandbox.\n" "- Use TOOL:PERMISSION before destructive or irreversible operations.\n" + "- Use TOOL:CURL to fetch documentation, APIs, or web content you need.\n" "- Reason step-by-step inside <|think|>… (hidden from user).\n" "- After each tool call, explain what you did in plain English.\n\n"; - for (const auto &kf : knowledge_files) { std::ifstream f(kf); if (!f) continue; @@ -1099,7 +1595,6 @@ static std::string build_system_prompt(const std::vector &knowledge // ═══════════════════════════════════════════════════════════════════════════ // Slash command handler // ═══════════════════════════════════════════════════════════════════════════ - static void handle_slash(const std::string &input, NitroConfig &cfg, AgentState &agent, @@ -1116,9 +1611,10 @@ static void handle_slash(const std::string &input, tui.append_line("[sys] Commands:"); tui.append_line("[sys] /model load a GGUF model"); tui.append_line("[sys] /embed load an embedding model for RAG"); - tui.append_line("[sys] /rag index file or directory"); + tui.append_line("[sys] /rag [path] index file or directory (picker if no path)"); tui.append_line("[sys] /memory KV / VRAM / layer stats"); tui.append_line("[sys] /clear reset conversation"); + tui.append_line("[sys] /settings show current settings"); tui.append_line("[sys] /help this message"); tui.append_line("[sys] exit / quit exit Nitro"); tui.redraw_all(); @@ -1134,6 +1630,7 @@ static void handle_slash(const std::string &input, if (agent.setup_model(cfg, tui)) { std::string sysp = build_system_prompt(cfg.knowledge_files, cfg.sandbox); agent.reset_conversation(sysp, tui); + save_settings(cfg); } tui.redraw_all(); return; @@ -1145,16 +1642,26 @@ static void handle_slash(const std::string &input, tui.redraw_all(); return; } cfg.embed_path = rest; - agent.setup_embed(rest, tui); + if (agent.setup_embed(rest, tui)) { + save_settings(cfg); + } return; } if (verb == "/rag") { - if (rest.empty()) { - tui.append_line("[err] Usage: /rag "); - tui.redraw_all(); return; + std::string path = rest; + if (path.empty()) { + // Launch the interactive folder picker starting from the sandbox. + path = tui.rag_folder_picker(cfg.sandbox); + if (path.empty()) { + tui.append_line("[sys] RAG indexing cancelled."); + tui.redraw_all(); + return; + } } - agent.rag_index(rest, tui); + tui.append_line("[sys] Indexing: " + path); + tui.redraw_all(); + agent.rag_index(path, tui); return; } @@ -1176,20 +1683,80 @@ static void handle_slash(const std::string &input, return; } + if (verb == "/settings") { + tui.append_line("[sys] Current settings:"); + tui.append_line("[sys] model_path : " + cfg.model_path); + tui.append_line("[sys] embed_path : " + cfg.embed_path); + tui.append_line("[sys] sandbox : " + cfg.sandbox); + tui.append_line("[sys] n_ctx : " + std::to_string(cfg.n_ctx)); + tui.append_line("[sys] n_gpu_layers : " + std::to_string(cfg.n_gpu_layers)); + tui.append_line("[sys] n_max_tokens : " + std::to_string(cfg.n_max_tokens)); + tui.append_line("[sys] temperature : " + std::to_string(cfg.temperature)); + tui.append_line("[sys] top_p : " + std::to_string(cfg.top_p)); + tui.append_line("[sys] top_k : " + std::to_string(cfg.top_k)); + tui.append_line("[sys] penalty_repeat: " + std::to_string(cfg.penalty_repeat)); + tui.append_line("[sys] rag_top_k : " + std::to_string(cfg.rag_top_k)); + tui.append_line("[sys] saved to : " + settings_path()); + tui.redraw_all(); + return; + } + tui.append_line("[err] Unknown command: " + verb + " (try /help)"); tui.redraw_all(); } // ═══════════════════════════════════════════════════════════════════════════ -// Welcome banner +// Welcome banner β€” colourful multi-line ASCII logo // ═══════════════════════════════════════════════════════════════════════════ - static void welcome(TuiState &tui, const std::string &sandbox) { - tui.append_line("[sys] ╔═══════════════════════════════════════════╗"); - tui.append_line("[sys] β•‘ N I T R O A G E N T v1.0 β•‘"); - tui.append_line("[sys] β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•"); - tui.append_line("[sys] Sandbox : " + sandbox); - tui.append_line("[sys] /help for commands Β· exit to quit"); + // Each line is tagged with a [sys] prefix so redraw_chat applies the + // right channel (chat_ch 140,140,200). We print the logo directly onto + // the chat plane here; subsequent redraws will re-render it from the + // chat_lines vector, which is fine. + // + // The logo uses block characters and Unicode box-drawing; it degrades + // gracefully to plain ASCII on terminals that don't support them. + // + // Colour coding: each row gets a different fg colour via a small gradient + // from cyan β†’ magenta so it looks "fancy" without requiring custom planes. + // Because append_line uses the [sys] prefix colour rule for all rows that + // start with "[sys]", we sneak the colour in by using a small ANSI-escape- + // free approach: we write logo lines without the "[sys]" prefix and colour + // them with the generic chat_ch(210,210,210). We then set their text so + // redraw_chat picks them up with the right colour rule. + // + // The easiest approach: use special prefix "[logo]" β†’ handled in + // redraw_chat just like [sys] but with a different per-row colour. + // To avoid touching redraw_chat, we instead pick a gradient and embed it + // directly into the strings, relying on notcurses to render the UTF-8 + // box-art as-is (no ANSI escapes β€” notcurses owns the terminal). + + // We append each logo row as a "[logo_N]" marker that redraw_chat will + // colour with a gradient. We handle this by using a small local helper + // that picks a colour based on the row index. + + // ── Logo rows (pure text, no ANSI) ────────────────────────────────── + // The prefix "⚑" keeps the colour rule from matching "[sys]" etc. + // redraw_chat: any line that doesn't match known prefixes gets the + // default colour (210,210,210). We rely on that for most logo rows. + // + // To get a gradient we insert a thin Ξ»-wrapper that adds lines with + // distinct prefixes we interpret in redraw_chat. But that requires + // editing redraw_chat β€” so instead we just use [sys] lines with + // embedded Unicode art; they all get the same blue-ish colour which + // still looks great. + + tui.append_line(""); + tui.append_line("[sys] β–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•—β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— "); + tui.append_line("[sys] β–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘β•šβ•β•β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•—"); + tui.append_line("[sys] β–ˆβ–ˆβ•”β–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘"); + tui.append_line("[sys] β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘"); + tui.append_line("[sys] β–ˆβ–ˆβ•‘ β•šβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•"); + tui.append_line("[sys] β•šβ•β• β•šβ•β•β•β•β•šβ•β• β•šβ•β• β•šβ•β• β•šβ•β• β•šβ•β•β•β•β•β• "); + tui.append_line("[sys] ─────────── agentic LLM shell v1.0 ────────────"); + tui.append_line(""); + tui.append_line("[sys] Sandbox : " + sandbox); + tui.append_line("[sys] /help for commands Β· exit to quit"); tui.append_line(""); tui.redraw_all(); } @@ -1197,18 +1764,12 @@ static void welcome(TuiState &tui, const std::string &sandbox) { // ═══════════════════════════════════════════════════════════════════════════ // main() // ═══════════════════════════════════════════════════════════════════════════ - int main(int argc, char **argv) { + // ── Load persisted settings first (provides defaults) ──────────── NitroConfig cfg; + load_settings(cfg); // silently no-ops if ~/.config/nitro.settings.json absent - // ── Parse arguments ─────────────────────────────────────────────── - // Accepted forms: - // ./nitro [options] [project_dir] - // -m / --model GGUF to load - // -e / --embed embedding model - // -g / --gpu-layers GPU layer count - // The first non-option argument is treated as project_dir. - + // ── Parse arguments (command-line overrides saved settings) ────── auto resolve_path = [](const std::string &arg) -> std::string { std::error_code ec; if (arg.substr(0, 2) == "~/") { @@ -1222,7 +1783,6 @@ int main(int argc, char **argv) { for (int i = 1; i < argc; ++i) { std::string a = argv[i]; - auto take_next = [&](const char *flag) -> std::string { if (i + 1 >= argc) { std::fprintf(stderr, "nitro: %s requires an argument\n", flag); @@ -1230,7 +1790,6 @@ int main(int argc, char **argv) { } return argv[++i]; }; - if (a == "-m" || a == "--model") { cfg.model_path = resolve_path(take_next(a.c_str())); } else if (a == "-e" || a == "--embed") { @@ -1248,12 +1807,14 @@ int main(int argc, char **argv) { " -h, --help show this help\n" "\n" "project_dir defaults to the current working directory.\n" + "Settings are persisted to ~/.config/nitro.settings.json.\n" "\n" "Slash commands inside nitro:\n" " /model load / hot-reload a GGUF\n" " /embed load an embedding model\n" - " /rag index file or directory\n" + " /rag [path] index file or directory (picker if no path)\n" " /memory KV / VRAM / layer stats\n" + " /settings show current settings\n" " /clear reset conversation\n" " /help list commands\n" ); @@ -1262,7 +1823,6 @@ int main(int argc, char **argv) { std::fprintf(stderr, "nitro: unknown option '%s' (try --help)\n", a.c_str()); std::exit(1); } else { - // positional β†’ project_dir cfg.sandbox = resolve_path(a); } } @@ -1279,28 +1839,24 @@ int main(int argc, char **argv) { if (fs::exists(kf)) cfg.knowledge_files.push_back(kf); } + // ── Init curl globally ──────────────────────────────────────────── + curl_global_init(CURL_GLOBAL_DEFAULT); + // ── Init TUI ────────────────────────────────────────────────────── TuiState tui; tui.init(); welcome(tui, cfg.sandbox); // ── Init agent ──────────────────────────────────────────────────── - // AgentState owns a Llama whose constructor calls llama_backend_init(); - // its destructor calls llama_backend_free() β€” nitro never touches - // the raw llama API directly. AgentState agent; - if (!cfg.model_path.empty()) { - // Model provided on the command line β€” load immediately. if (agent.setup_model(cfg, tui)) { std::string sysp = build_system_prompt(cfg.knowledge_files, cfg.sandbox); agent.reset_conversation(sysp, tui); } - // Load embedding model if also provided up-front. if (!cfg.embed_path.empty()) agent.setup_embed(cfg.embed_path, tui); } else { - // No model yet β€” friendly prompt, not an error. tui.append_line("[sys] No model specified. Use /model to load one."); tui.append_line("[sys] Example: /model ~/models/qwen2.5-7b-q4_k_m.gguf"); tui.redraw_all(); @@ -1308,26 +1864,20 @@ int main(int argc, char **argv) { // ── Main loop ───────────────────────────────────────────────────── for (;;) { - // Check for terminal resize { unsigned rows = 0, cols = 0; notcurses_stddim_yx(tui.nc, &rows, &cols); if ((int)rows != tui.term_rows || (int)cols != tui.term_cols) tui.resize(); } - std::string input = tui.readline_blocking(); - // trim input.erase(0, input.find_first_not_of(" \t")); if (!input.empty()) input.erase(input.find_last_not_of(" \t\r\n") + 1); if (input.empty()) continue; - tui.append_line("You: " + input); tui.redraw_all(); - if (input == "exit" || input == "quit") break; - if (input[0] == '/') { handle_slash(input, cfg, agent, tui); } else { @@ -1336,6 +1886,6 @@ int main(int argc, char **argv) { } tui.destroy(); - // agent destructor cleans up Llama (which calls llama_backend_free) + curl_global_cleanup(); return 0; } From 6436a27f363efca8096b88397f27d4a96fef6cba Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Thu, 21 May 2026 17:42:35 +0930 Subject: [PATCH 112/131] LLAMA: nitro agent - wip --- llama/nitro.cpp | 562 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 412 insertions(+), 150 deletions(-) diff --git a/llama/nitro.cpp b/llama/nitro.cpp index cd63674..9a7424f 100644 --- a/llama/nitro.cpp +++ b/llama/nitro.cpp @@ -71,9 +71,10 @@ static std::string list_dir(const std::string &path); static bool path_in_sandbox(const std::string &sandbox, const std::string &path); static std::string strip_code_fences(const std::string &filename, const std::string &src); static std::string process_tool(const std::string &line, const std::string &sandbox, - TuiState &tui); + const std::vector &run_allowed, + TuiState &tui); static std::string build_system_prompt(const std::vector &knowledge_files, - const std::string &sandbox); + const std::string &sandbox); // ─── RAG indexing ───────────────────────────────────────────────────────────── static constexpr int BATCH_SIZE = 512; @@ -209,6 +210,45 @@ class InputHistory { current_index = (int)history_stack.size(); } + /** + * @brief Load history from ~/.config/nitro.history (one entry per line). + * Silently succeeds if the file doesn't exist. + */ + void load(const std::string &path) { + std::ifstream f(path); + if (!f) return; + std::string line; + while (std::getline(f, line)) { + if (!line.empty()) history_stack.push_back(line); + } + current_index = (int)history_stack.size(); + } + + /** + * @brief Persist history to disk (most-recent last, one entry per line). + * Caps at MAX_PERSIST entries so the file never grows unbounded. + */ + void save(const std::string &path) const { + // Ensure parent directory exists. + fs::path dir = fs::path(path).parent_path(); + std::error_code ec; + fs::create_directories(dir, ec); + + std::ofstream f(path, std::ios::trunc); + if (!f) return; + + static constexpr int MAX_PERSIST = 500; + int start = std::max(0, (int)history_stack.size() - MAX_PERSIST); + for (int i = start; i < (int)history_stack.size(); ++i) { + // Escape embedded newlines so each entry stays on one line. + for (char c : history_stack[i]) { + if (c == '\n') f << "\\n"; + else f << c; + } + f << '\n'; + } + } + private: std::vector history_stack; int current_index = 0; @@ -237,6 +277,9 @@ struct NitroConfig { int penalty_last_n = 256; std::vector knowledge_files; int rag_top_k = 5; + // TOOL:RUN allowlist β€” if non-empty, only these program basenames may run. + // Empty means "allow anything inside the sandbox" (original behaviour). + std::vector run_allowed; }; // Returns the canonical settings path: ~/.config/nitro.settings.json @@ -246,17 +289,24 @@ static std::string settings_path() { return base + "/.config/nitro.settings.json"; } +// Returns the history file path: ~/.config/nitro.history +static std::string history_path() { + const char *home = getenv("HOME"); + std::string base = home ? std::string(home) : "."; + return base + "/.config/nitro.history"; +} + // Tiny helper: extract a quoted string value from flat JSON for a known key. static bool settings_get_str(const std::string &json, - const std::string &key, - std::string &out) { + const std::string &key, + std::string &out) { return json_get_string(json, key, out); } // Tiny helper: extract an integer value from flat JSON. static bool settings_get_int(const std::string &json, - const std::string &key, - int &out) { + const std::string &key, + int &out) { std::string search = "\"" + key + "\":"; size_t pos = json.find(search); if (pos == std::string::npos) return false; @@ -274,8 +324,8 @@ static bool settings_get_int(const std::string &json, // Tiny helper: extract a float value from flat JSON. static bool settings_get_float(const std::string &json, - const std::string &key, - float &out) { + const std::string &key, + float &out) { std::string search = "\"" + key + "\":"; size_t pos = json.find(search); if (pos == std::string::npos) return false; @@ -327,11 +377,11 @@ static std::string json_escape(const std::string &s) { out.reserve(s.size() + 4); for (char c : s) { switch (c) { - case '"': out += "\\\""; break; - case '\\': out += "\\\\"; break; - case '\n': out += "\\n"; break; - case '\t': out += "\\t"; break; - default: out += c; break; + case '"': out += "\\\""; break; + case '\\': out += "\\\\"; break; + case '\n': out += "\\n"; break; + case '\t': out += "\\t"; break; + default: out += c; break; } } return out; @@ -472,7 +522,7 @@ void TuiState::init() { stdpl = notcurses_stdplane(nc); notcurses_term_dim_yx(nc, (unsigned *)&term_rows, (unsigned *)&term_cols); uint64_t bg = NCCHANNELS_INITIALIZER(BG_CHAT_R, BG_CHAT_G, BG_CHAT_B, - BG_CHAT_R, BG_CHAT_G, BG_CHAT_B); + BG_CHAT_R, BG_CHAT_G, BG_CHAT_B); ncplane_set_base(stdpl, " ", 0, bg); ncplane_erase(stdpl); ncplane_options hopt{}; @@ -485,15 +535,15 @@ void TuiState::init() { copt.rows = (unsigned)chat_rows; copt.cols = (unsigned)term_cols; chatpl = ncplane_create(stdpl, &copt); ncplane_set_base(chatpl, " ", 0, - NCCHANNELS_INITIALIZER(BG_CHAT_R, BG_CHAT_G, BG_CHAT_B, - BG_CHAT_R, BG_CHAT_G, BG_CHAT_B)); + NCCHANNELS_INITIALIZER(BG_CHAT_R, BG_CHAT_G, BG_CHAT_B, + BG_CHAT_R, BG_CHAT_G, BG_CHAT_B)); ncplane_options iopt{}; iopt.y = term_rows - 2; iopt.x = 0; iopt.rows = 2; iopt.cols = (unsigned)term_cols; inputpl = ncplane_create(stdpl, &iopt); ncplane_set_base(inputpl, " ", 0, - NCCHANNELS_INITIALIZER(BG_INP_R, BG_INP_G, BG_INP_B, - BG_INP_R, BG_INP_G, BG_INP_B)); + NCCHANNELS_INITIALIZER(BG_INP_R, BG_INP_G, BG_INP_B, + BG_INP_R, BG_INP_G, BG_INP_B)); redraw_all(); } @@ -515,17 +565,17 @@ void TuiState::resize() { void TuiState::redraw_header() { ncplane_erase(header); ncplane_set_base(header, " ", 0, - NCCHANNELS_INITIALIZER(BG_HDR_R, BG_HDR_G, BG_HDR_B, - BG_HDR_R, BG_HDR_G, BG_HDR_B)); + NCCHANNELS_INITIALIZER(BG_HDR_R, BG_HDR_G, BG_HDR_B, + BG_HDR_R, BG_HDR_G, BG_HDR_B)); float kv_pct = kv_total > 0 ? 100.f * (float)kv_used / (float)kv_total : 0.f; float vram_pct = vram_total > 0 ? 100.f * (float)vram_used / (float)vram_total : 0.f; static const char *const SPIN[] = { "β£Ύ","β£½","β£»","β’Ώ","β‘Ώ","⣟","β£―","β£·" }; const char *spin_str = thinking ? SPIN[spinner_frame % 8] : " "; char buf[512]; int n = std::snprintf(buf, sizeof(buf), - " ✦ NITRO β”‚ %-32s β”‚ %5.1f tok/s β”‚ KV %4.1f%% VRAM %4.1f%% %s", - current_model.c_str(), (double)tokens_per_sec, - (double)kv_pct, (double)vram_pct, spin_str); + " ✦ NITRO β”‚ %-32s β”‚ %5.1f tok/s β”‚ KV %4.1f%% VRAM %4.1f%% %s", + current_model.c_str(), (double)tokens_per_sec, + (double)kv_pct, (double)vram_pct, spin_str); if (n > term_cols) buf[term_cols] = '\0'; ncplane_set_channels(header, hdr_ch(130, 220, 200)); ncplane_putstr_yx(header, 0, 0, buf); @@ -543,14 +593,29 @@ void TuiState::redraw_chat() { for (int i = start, row = 0; i < end; ++i, ++row) { const std::string &line = chat_lines[i]; uint64_t ch; - if (line.rfind("You: ", 0) == 0) ch = chat_ch(100, 200, 255); + // Logo lines use prefix "[logo_N]" where N is the row index 0-6. + // We interpolate a cyanβ†’magenta gradient across the 7 art rows. + if (line.rfind("[logo_", 0) == 0 && line.size() > 7 && line[7] == ']') { + int logo_row = line[6] - '0'; + // Gradient: cyan (0,230,255) β†’ green (80,255,160) β†’ magenta (220,80,255) + // 7 steps, indices 0-6. + static const uint32_t GRAD_R[] = { 0, 20, 60, 120, 180, 210, 220 }; + static const uint32_t GRAD_G[] = { 230, 255, 255, 255, 200, 130, 80 }; + static const uint32_t GRAD_B[] = { 255, 200, 140, 80, 100, 200, 255 }; + int gi = std::max(0, std::min(logo_row, 6)); + ch = chat_ch(GRAD_R[gi], GRAD_G[gi], GRAD_B[gi]); + } + else if (line.rfind("You: ", 0) == 0) ch = chat_ch(100, 200, 255); else if (line.rfind("Nitro: ", 0) == 0) ch = chat_ch(180, 255, 180); else if (line.rfind("[tool]", 0) == 0) ch = chat_ch(255, 180, 80); else if (line.rfind("[err]", 0) == 0) ch = chat_ch(255, 80, 80); else if (line.rfind("[sys]", 0) == 0) ch = chat_ch(140, 140, 200); else ch = chat_ch(210, 210, 210); ncplane_set_channels(chatpl, ch); - std::string display = line.size() > cols ? line.substr(0, cols) : line; + // Strip the [logo_N] prefix before rendering. + std::string display = (line.rfind("[logo_", 0) == 0 && line.size() > 8) + ? line.substr(8) : line; + if (display.size() > cols) display = display.substr(0, cols); ncplane_putstr_yx(chatpl, row, 0, display.c_str()); } } @@ -575,14 +640,14 @@ void TuiState::redraw_input() { cur_in_view = std::min(cur_in_view, (int)visible.size()); std::string before = visible.substr(0, cur_in_view); std::string after = cur_in_view < (int)visible.size() - ? visible.substr(cur_in_view + 1) : ""; + ? visible.substr(cur_in_view + 1) : ""; char cursor_ch_val = cur_in_view < (int)visible.size() - ? visible[cur_in_view] : ' '; + ? visible[cur_in_view] : ' '; ncplane_set_channels(inputpl, inp_ch(230, 230, 230)); ncplane_putstr_yx(inputpl, 1, prompt_cols, before.c_str()); int cx = prompt_cols + cur_in_view; ncplane_set_channels(inputpl, - NCCHANNELS_INITIALIZER(BG_INP_R, BG_INP_G, BG_INP_B, 180, 230, 255)); + NCCHANNELS_INITIALIZER(BG_INP_R, BG_INP_G, BG_INP_B, 180, 230, 255)); char cbuf[2] = { cursor_ch_val, '\0' }; ncplane_putstr_yx(inputpl, 1, cx, cbuf); ncplane_set_channels(inputpl, inp_ch(230, 230, 230)); @@ -667,7 +732,7 @@ void TuiState::show_modal_popup(const std::string &message) { // Background: deep navy. static constexpr uint32_t PBG_R = 20, PBG_G = 28, PBG_B = 50; ncplane_set_base(modal_plane, " ", 0, - NCCHANNELS_INITIALIZER(PBG_R, PBG_G, PBG_B, PBG_R, PBG_G, PBG_B)); + NCCHANNELS_INITIALIZER(PBG_R, PBG_G, PBG_B, PBG_R, PBG_G, PBG_B)); ncplane_erase(modal_plane); // Border β€” bright cyan. @@ -702,8 +767,8 @@ void TuiState::show_modal_popup(const std::string &message) { // Truncate message to fit inside border. int max_msg = popup_w - 4; std::string display = message.size() > (size_t)max_msg - ? message.substr(0, max_msg) - : message; + ? message.substr(0, max_msg) + : message; ncplane_putstr_yx(modal_plane, 2, 2, display.c_str()); notcurses_render(nc); @@ -732,7 +797,7 @@ std::string TuiState::rag_folder_picker(const std::string &start_dir) { // Build an entry list for the current directory. auto load_entries = [](const std::string &dir, - std::vector &entries) { + std::vector &entries) { entries.clear(); std::error_code ec; // Add ".." for going up (except at fs root). @@ -771,7 +836,7 @@ std::string TuiState::rag_folder_picker(const std::string &start_dir) { static constexpr uint32_t PBG_R = 18, PBG_G = 24, PBG_B = 40; ncplane_set_base(picker, " ", 0, - NCCHANNELS_INITIALIZER(PBG_R, PBG_G, PBG_B, PBG_R, PBG_G, PBG_B)); + NCCHANNELS_INITIALIZER(PBG_R, PBG_G, PBG_B, PBG_R, PBG_G, PBG_B)); auto draw_picker = [&]() { ncplane_erase(picker); @@ -791,7 +856,7 @@ std::string TuiState::rag_folder_picker(const std::string &start_dir) { // Title ncplane_set_channels(picker, - NCCHANNELS_INITIALIZER(255, 220, 80, PBG_R, PBG_G, PBG_B)); + NCCHANNELS_INITIALIZER(255, 220, 80, PBG_R, PBG_G, PBG_B)); ncplane_putstr_yx(picker, 0, 2, " πŸ“‚ RAG Folder Picker "); // Current path (truncated to fit) @@ -799,14 +864,14 @@ std::string TuiState::rag_folder_picker(const std::string &start_dir) { if ((int)path_display.size() > PW - 4) path_display = "…" + path_display.substr(path_display.size() - (PW - 5)); ncplane_set_channels(picker, - NCCHANNELS_INITIALIZER(160, 200, 240, PBG_R, PBG_G, PBG_B)); + NCCHANNELS_INITIALIZER(160, 200, 240, PBG_R, PBG_G, PBG_B)); ncplane_putstr_yx(picker, 1, 2, path_display.c_str()); // Hint line ncplane_set_channels(picker, - NCCHANNELS_INITIALIZER(120, 120, 160, PBG_R, PBG_G, PBG_B)); + NCCHANNELS_INITIALIZER(120, 120, 160, PBG_R, PBG_G, PBG_B)); ncplane_putstr_yx(picker, PH - 2, 2, - "↑↓ navigate Enter open s=select dir Esc cancel"); + "↑↓ navigate Enter open s=select dir Esc cancel"); // Entry list int list_rows = PH - 5; // rows 2 … PH-4 available @@ -827,7 +892,7 @@ std::string TuiState::rag_folder_picker(const std::string &start_dir) { uint32_t bg = is_selected ? 180 : PBG_G; uint32_t bb = is_selected ? 255 : PBG_B; ncplane_set_channels(picker, - NCCHANNELS_INITIALIZER(fr, fg, fb, br, bg, bb)); + NCCHANNELS_INITIALIZER(fr, fg, fb, br, bg, bb)); // Pad entry to fill width std::string label = (is_selected ? " β–Ά " : " ") + entries[idx]; if ((int)label.size() > PW - 2) label = label.substr(0, PW - 2); @@ -1091,7 +1156,7 @@ bool AgentState::setup_model(const NitroConfig &cfg, TuiState &tui) { llama.reset(); apply_generation_params(cfg); if (!llama.load_model(cfg.model_path, cfg.n_ctx, cfg.n_batch, - cfg.n_gpu_layers, cfg.log_level)) { + cfg.n_gpu_layers, cfg.log_level)) { tui.dismiss_modal_popup(); tui.append_line(std::string("[err] ") + llama.last_error()); tui.redraw_all(); @@ -1196,8 +1261,8 @@ bool AgentState::rag_index(const std::string &path, TuiState &tui) { // Agent turn // ═══════════════════════════════════════════════════════════════════════════ bool AgentState::run_turn(const std::string &user_message, - const NitroConfig &cfg, - TuiState &tui) { + const NitroConfig &cfg, + TuiState &tui) { if (!model_loaded) { tui.append_line("[err] No model loaded. Use /model "); tui.redraw_all(); @@ -1206,7 +1271,7 @@ bool AgentState::run_turn(const std::string &user_message, std::string effective_message = user_message; if (embed_llama && rag_db && rag_session) { std::string context = llama.rag_retrieve(*rag_db, user_message, - cfg.rag_top_k, *rag_session); + cfg.rag_top_k, *rag_session); if (!context.empty()) { effective_message = "Context:\n" + context + "\n\nUser: " + user_message; } @@ -1292,9 +1357,9 @@ bool AgentState::run_turn(const std::string &user_message, tui.append_line("[tool] " + op + " " + arg1 + (op == "TOOL:WRITE" ? " " : "")); tui.redraw_all(); - std::string result = process_tool(tool_line, cfg.sandbox, tui); + std::string result = process_tool(tool_line, cfg.sandbox, cfg.run_allowed, tui); tui.append_line("[tool] β†’ " + - result.substr(0, 200) + (result.size() > 200 ? "…" : "")); + result.substr(0, 200) + (result.size() > 200 ? "…" : "")); tui.redraw_all(); if (!llama.add_message(*iter, "tool", result)) { tui.append_line(std::string("[err] tool result inject: ") + llama.last_error()); @@ -1314,7 +1379,7 @@ bool AgentState::run_turn(const std::string &user_message, std::string trimmed = buffer; trimmed.erase(0, trimmed.find_first_not_of(" \t")); if (trimmed.substr(0, 5) == "TOOL:") { - std::string result = process_tool(trimmed, cfg.sandbox, tui); + std::string result = process_tool(trimmed, cfg.sandbox, cfg.run_allowed, tui); tui.append_line("[tool] β†’ " + result.substr(0, 200)); tui.redraw_all(); llama.add_message(*iter, "tool", result); @@ -1335,9 +1400,9 @@ bool AgentState::run_turn(const std::string &user_message, char stat[128]; std::snprintf(stat, sizeof(stat), "[sys] %.1f tok/s (%d tokens) KV %.1f%%", - (double)tui.tokens_per_sec, - iter->_tokens_generated, - (double)mem.kv_percent); + (double)tui.tokens_per_sec, + iter->_tokens_generated, + (double)mem.kv_percent); tui.append_line(stat); tui.redraw_all(); return true; @@ -1401,10 +1466,10 @@ static const std::vector CODE_EXTENSIONS = { }; static std::string strip_code_fences(const std::string &filename, - const std::string &src) { + const std::string &src) { auto ext = fs::path(filename).extension().string(); bool is_code = std::any_of(CODE_EXTENSIONS.begin(), CODE_EXTENSIONS.end(), - [&](const std::string &e){ return ext == e; }); + [&](const std::string &e){ return ext == e; }); if (!is_code) return src; auto pos = src.find("```"); if (pos == std::string::npos) return src; @@ -1416,6 +1481,137 @@ static std::string strip_code_fences(const std::string &filename, return inner; } +// ═══════════════════════════════════════════════════════════════════════════ +// html_to_text β€” strip HTML for cleaner TOOL:CURL context +// ═══════════════════════════════════════════════════════════════════════════ +// Lightweight HTMLβ†’plain-text conversion: +// β€’ Drops , and + for (const std::string &tag : {"script", "style"}) { + std::string open = "<" + tag; + std::string close = ""; + std::string lo = s; + std::transform(lo.begin(), lo.end(), lo.begin(), ::tolower); + for (;;) { + auto p0 = lo.find(open); + if (p0 == std::string::npos) break; + auto p1 = lo.find(close, p0); + if (p1 == std::string::npos) { s.erase(p0); lo.erase(p0); break; } + s.erase(p0, p1 + close.size() - p0); + lo.erase(p0, p1 + close.size() - p0); + } + } + + // 3. Replace block-level tags with '\n' before stripping all tags. + static const char *const BLOCK[] = { + "p","div","br","li","tr","h1","h2","h3","h4","h5","h6", + "article","section","header","footer","nav","main", nullptr + }; + { + std::string out; + out.reserve(s.size()); + size_t i = 0; + while (i < s.size()) { + if (s[i] != '<') { out += s[i++]; continue; } + auto ce = s.find('>', i); + if (ce == std::string::npos) { out += s[i++]; continue; } + std::string inner = s.substr(i + 1, ce - i - 1); + size_t sp = inner.find_first_of(" \t/\r\n"); + std::string name = (sp != std::string::npos) ? inner.substr(0, sp) : inner; + std::transform(name.begin(), name.end(), name.begin(), ::tolower); + for (int k = 0; BLOCK[k]; ++k) + if (name == BLOCK[k]) { out += '\n'; break; } + i = ce + 1; + } + s = out; + } + + // 4. Strip all remaining tags. + { + std::string out; out.reserve(s.size()); + bool in_tag = false; + for (char c : s) { + if (c == '<') { in_tag = true; continue; } + if (c == '>') { in_tag = false; continue; } + if (!in_tag) out += c; + } + s = out; + } + + // 5. Decode common HTML entities. + static const std::pair ENT[] = { + {"&","&"},{"<","<"},{">",">"},{""","\""}, + {"'","'"},{" "," "},{"—","β€”"},{"–","–"}, + {"…","…"},{"'","'"},{""","\""}, + {nullptr,nullptr} + }; + for (int k = 0; ENT[k].first; ++k) { + std::string e = ENT[k].first, r = ENT[k].second; + size_t pos = 0; + while ((pos = s.find(e, pos)) != std::string::npos) + { s.replace(pos, e.size(), r); pos += r.size(); } + } + // Numeric entities &#NNN; and &#xHHH; + { + std::string out; out.reserve(s.size()); + size_t i = 0; + while (i < s.size()) { + if (s[i]=='&' && i+2>6)); out += (char)(0x80|(cp&0x3F)); } + else { out += (char)(0xE0|(cp>>12)); out += (char)(0x80|((cp>>6)&0x3F)); out += (char)(0x80|(cp&0x3F)); } + i = semi+1; continue; + } catch (...) {} + } + } + out += s[i++]; + } + s = out; + } + + // 6. Collapse whitespace; cap blank lines at 2. + { + std::string out; out.reserve(s.size()); + int nl_run = 0; bool last_sp = false; + for (char c : s) { + if (c == '\r') continue; + if (c == '\t') c = ' '; + if (c == '\n') { ++nl_run; last_sp=false; if (nl_run<=2) out+='\n'; continue; } + nl_run = 0; + if (c == ' ') { if (!last_sp) { out+=' '; last_sp=true; } continue; } + last_sp = false; out += c; + } + size_t f = out.find_first_not_of(" \n"); + size_t l = out.find_last_not_of(" \n"); + s = (f == std::string::npos) ? "" : out.substr(f, l-f+1); + } + return s; +} + // ═══════════════════════════════════════════════════════════════════════════ // TOOL:CURL β€” HTTP GET with libcurl, returns body text (capped at 32 KB). // ═══════════════════════════════════════════════════════════════════════════ @@ -1453,6 +1649,14 @@ static std::string tool_curl(const std::string &url) { CURLcode res = curl_easy_perform(curl); long http_code = 0; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + + // Query content-type before cleanup (pointer is only valid while handle lives). + char *ct_raw = nullptr; + curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &ct_raw); + std::string content_type = ct_raw ? ct_raw : ""; + std::transform(content_type.begin(), content_type.end(), + content_type.begin(), ::tolower); + curl_easy_cleanup(curl); if (res != CURLE_OK) { @@ -1462,6 +1666,13 @@ static std::string tool_curl(const std::string &url) { return "ERROR: HTTP " + std::to_string(http_code) + " from " + url; } if (body.empty()) return "(empty response)"; + + // Strip HTML tags so the model receives clean plain text. + bool is_html = (content_type.find("text/html") != std::string::npos) + || (body.size() > 5 && body.substr(0,5) == " 6 && body.substr(0,6) == ""); + if (is_html) body = html_to_text(body); + return body; } @@ -1469,8 +1680,9 @@ static std::string tool_curl(const std::string &url) { // Tool dispatch // ═══════════════════════════════════════════════════════════════════════════ static std::string process_tool(const std::string &cmd, - const std::string &sandbox, - TuiState &tui) { + const std::string &sandbox, + const std::vector &run_allowed, + TuiState &tui) { std::string op, arg1, arg2; auto sp1 = cmd.find(' '); if (sp1 == std::string::npos) { @@ -1528,7 +1740,7 @@ static std::string process_tool(const std::string &cmd, if (!path_in_sandbox(sandbox, p)) return "ERROR: path outside sandbox"; std::string content = strip_code_fences(arg1, arg2); return write_file(p, content) ? "OK: written to " + arg1 - : "ERROR: write failed for " + arg1; + : "ERROR: write failed for " + arg1; } if (op == "TOOL:PERMISSION") { std::string result; @@ -1542,6 +1754,16 @@ static std::string process_tool(const std::string &cmd, if (op == "TOOL:RUN") { std::string prog = resolve(arg1); if (!path_in_sandbox(sandbox, prog)) return "ERROR: path outside sandbox"; + // Enforce allowlist if one is configured. + if (!run_allowed.empty()) { + std::string basename = fs::path(prog).filename().string(); + bool permitted = std::any_of(run_allowed.begin(), run_allowed.end(), + [&](const std::string &a){ return a == basename; }); + if (!permitted) { + return "ERROR: '" + basename + "' is not in the TOOL:RUN allowlist. " + "Use /set run_allowed to permit it."; + } + } std::string command = prog + " " + arg2 + " 2>&1"; FILE *fp = popen(command.c_str(), "r"); if (!fp) return "ERROR: popen failed"; @@ -1559,30 +1781,30 @@ static std::string process_tool(const std::string &cmd, // System prompt // ═══════════════════════════════════════════════════════════════════════════ static std::string build_system_prompt(const std::vector &knowledge_files, - const std::string &sandbox) { + const std::string &sandbox) { std::string p; p += "You are Nitro, an agentic AI assistant for software development.\n" - "Your sandbox (project directory) is: " + sandbox + "\n\n" - "## Tool protocol\n" - "Emit tool calls on their own line. The host executes them and returns\n" - "TOOL_RESULT: on the next line.\n\n" - "Available tools:\n" - " TOOL:LIST [dir] list files (default: sandbox root)\n" - " TOOL:READ read file contents\n" - " TOOL:WRITE write text to file\n" - " TOOL:EXISTS YES or NO\n" - " TOOL:RUN [args] run program inside sandbox\n" - " TOOL:DATE current date\n" - " TOOL:TIME current time\n" - " TOOL:RND random float\n" - " TOOL:PERMISSION ask user for explicit permission\n" - " TOOL:CURL HTTP GET; returns response body (max 32 KB)\n\n" - "Rules:\n" - "- Never access files outside the sandbox.\n" - "- Use TOOL:PERMISSION before destructive or irreversible operations.\n" - "- Use TOOL:CURL to fetch documentation, APIs, or web content you need.\n" - "- Reason step-by-step inside <|think|>… (hidden from user).\n" - "- After each tool call, explain what you did in plain English.\n\n"; + "Your sandbox (project directory) is: " + sandbox + "\n\n" + "## Tool protocol\n" + "Emit tool calls on their own line. The host executes them and returns\n" + "TOOL_RESULT: on the next line.\n\n" + "Available tools:\n" + " TOOL:LIST [dir] list files (default: sandbox root)\n" + " TOOL:READ read file contents\n" + " TOOL:WRITE write text to file\n" + " TOOL:EXISTS YES or NO\n" + " TOOL:RUN [args] run program inside sandbox\n" + " TOOL:DATE current date\n" + " TOOL:TIME current time\n" + " TOOL:RND random float\n" + " TOOL:PERMISSION ask user for explicit permission\n" + " TOOL:CURL HTTP GET; returns response body (max 32 KB)\n\n" + "Rules:\n" + "- Never access files outside the sandbox.\n" + "- Use TOOL:PERMISSION before destructive or irreversible operations.\n" + "- Use TOOL:CURL to fetch documentation, APIs, or web content you need.\n" + "- Reason step-by-step inside <|think|>… (hidden from user).\n" + "- After each tool call, explain what you did in plain English.\n\n"; for (const auto &kf : knowledge_files) { std::ifstream f(kf); if (!f) continue; @@ -1596,9 +1818,9 @@ static std::string build_system_prompt(const std::vector &knowledge // Slash command handler // ═══════════════════════════════════════════════════════════════════════════ static void handle_slash(const std::string &input, - NitroConfig &cfg, - AgentState &agent, - TuiState &tui) { + NitroConfig &cfg, + AgentState &agent, + TuiState &tui) { auto sp = input.find(' '); std::string verb = (sp == std::string::npos) ? input : input.substr(0, sp); std::string rest; @@ -1609,14 +1831,19 @@ static void handle_slash(const std::string &input, if (verb == "/help") { tui.append_line("[sys] Commands:"); - tui.append_line("[sys] /model load a GGUF model"); - tui.append_line("[sys] /embed load an embedding model for RAG"); - tui.append_line("[sys] /rag [path] index file or directory (picker if no path)"); - tui.append_line("[sys] /memory KV / VRAM / layer stats"); - tui.append_line("[sys] /clear reset conversation"); - tui.append_line("[sys] /settings show current settings"); - tui.append_line("[sys] /help this message"); - tui.append_line("[sys] exit / quit exit Nitro"); + tui.append_line("[sys] /model load a GGUF model"); + tui.append_line("[sys] /embed load an embedding model for RAG"); + tui.append_line("[sys] /rag [path] index file or directory (picker if no path)"); + tui.append_line("[sys] /memory KV / VRAM / layer stats"); + tui.append_line("[sys] /clear reset conversation"); + tui.append_line("[sys] /settings show current settings"); + tui.append_line("[sys] /set change a setting live"); + tui.append_line("[sys] /help this message"); + tui.append_line("[sys] exit / quit exit Nitro"); + tui.append_line("[sys] Settable keys (via /set):"); + tui.append_line("[sys] temperature top_p top_k min_p penalty_repeat"); + tui.append_line("[sys] n_max_tokens penalty_last_n rag_top_k n_gpu_layers"); + tui.append_line("[sys] run_allowed (comma-separated list, e.g. python3,make)"); tui.redraw_all(); return; } @@ -1701,6 +1928,72 @@ static void handle_slash(const std::string &input, return; } + if (verb == "/set") { + // Usage: /set + // Parses the key and value, updates cfg in place, re-applies generation + // params if needed, and saves settings to disk. + auto sp2 = rest.find(' '); + std::string key = (sp2 == std::string::npos) ? rest : rest.substr(0, sp2); + std::string val = (sp2 == std::string::npos) ? "" : rest.substr(sp2 + 1); + val.erase(0, val.find_first_not_of(" \t")); + + if (key.empty() || val.empty()) { + tui.append_line("[err] Usage: /set "); + tui.redraw_all(); return; + } + + bool ok = true; + bool needs_reparam = false; // whether to re-apply generation params + + try { + if (key == "temperature") { cfg.temperature = std::stof(val); needs_reparam = true; } + else if (key == "top_p") { cfg.top_p = std::stof(val); needs_reparam = true; } + else if (key == "min_p") { cfg.min_p = std::stof(val); needs_reparam = true; } + else if (key == "top_k") { cfg.top_k = std::stoi(val); needs_reparam = true; } + else if (key == "penalty_repeat") { cfg.penalty_repeat = std::stof(val); needs_reparam = true; } + else if (key == "penalty_last_n") { cfg.penalty_last_n = std::stoi(val); needs_reparam = true; } + else if (key == "n_max_tokens") { cfg.n_max_tokens = std::stoi(val); needs_reparam = true; } + else if (key == "rag_top_k") { cfg.rag_top_k = std::stoi(val); } + else if (key == "n_gpu_layers") { + cfg.n_gpu_layers = std::stoi(val); + tui.append_line("[sys] n_gpu_layers will take effect on next /model load."); + } + else if (key == "run_allowed") { + // Accept a comma-separated list of basenames, or "none" to clear. + cfg.run_allowed.clear(); + if (val != "none") { + std::istringstream iss(val); + std::string tok; + while (std::getline(iss, tok, ',')) { + tok.erase(0, tok.find_first_not_of(" \t")); + tok.erase(tok.find_last_not_of(" \t") + 1); + if (!tok.empty()) cfg.run_allowed.push_back(tok); + } + } + if (cfg.run_allowed.empty()) { + tui.append_line("[sys] run_allowed cleared β€” all sandbox programs permitted."); + } else { + std::string list; + for (const auto &e : cfg.run_allowed) list += e + " "; + tui.append_line("[sys] run_allowed: " + list); + } + } + else { tui.append_line("[err] Unknown key '" + key + "'. Try /help for list."); ok = false; } + } catch (const std::exception &ex) { + tui.append_line(std::string("[err] /set: ") + ex.what()); + ok = false; + } + + if (ok) { + if (needs_reparam && agent.model_loaded) + agent.apply_generation_params(cfg); + save_settings(cfg); + tui.append_line("[sys] " + key + " = " + val); + } + tui.redraw_all(); + return; + } + tui.append_line("[err] Unknown command: " + verb + " (try /help)"); tui.redraw_all(); } @@ -1709,51 +2002,16 @@ static void handle_slash(const std::string &input, // Welcome banner β€” colourful multi-line ASCII logo // ═══════════════════════════════════════════════════════════════════════════ static void welcome(TuiState &tui, const std::string &sandbox) { - // Each line is tagged with a [sys] prefix so redraw_chat applies the - // right channel (chat_ch 140,140,200). We print the logo directly onto - // the chat plane here; subsequent redraws will re-render it from the - // chat_lines vector, which is fine. - // - // The logo uses block characters and Unicode box-drawing; it degrades - // gracefully to plain ASCII on terminals that don't support them. - // - // Colour coding: each row gets a different fg colour via a small gradient - // from cyan β†’ magenta so it looks "fancy" without requiring custom planes. - // Because append_line uses the [sys] prefix colour rule for all rows that - // start with "[sys]", we sneak the colour in by using a small ANSI-escape- - // free approach: we write logo lines without the "[sys]" prefix and colour - // them with the generic chat_ch(210,210,210). We then set their text so - // redraw_chat picks them up with the right colour rule. - // - // The easiest approach: use special prefix "[logo]" β†’ handled in - // redraw_chat just like [sys] but with a different per-row colour. - // To avoid touching redraw_chat, we instead pick a gradient and embed it - // directly into the strings, relying on notcurses to render the UTF-8 - // box-art as-is (no ANSI escapes β€” notcurses owns the terminal). - - // We append each logo row as a "[logo_N]" marker that redraw_chat will - // colour with a gradient. We handle this by using a small local helper - // that picks a colour based on the row index. - - // ── Logo rows (pure text, no ANSI) ────────────────────────────────── - // The prefix "⚑" keeps the colour rule from matching "[sys]" etc. - // redraw_chat: any line that doesn't match known prefixes gets the - // default colour (210,210,210). We rely on that for most logo rows. - // - // To get a gradient we insert a thin Ξ»-wrapper that adds lines with - // distinct prefixes we interpret in redraw_chat. But that requires - // editing redraw_chat β€” so instead we just use [sys] lines with - // embedded Unicode art; they all get the same blue-ish colour which - // still looks great. - + // Logo lines use the "[logo_N]" prefix so redraw_chat applies a + // per-row cyanβ†’magenta gradient (N = 0-6 maps to the gradient table). tui.append_line(""); - tui.append_line("[sys] β–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•—β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— "); - tui.append_line("[sys] β–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘β•šβ•β•β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•—"); - tui.append_line("[sys] β–ˆβ–ˆβ•”β–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘"); - tui.append_line("[sys] β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘"); - tui.append_line("[sys] β–ˆβ–ˆβ•‘ β•šβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•"); - tui.append_line("[sys] β•šβ•β• β•šβ•β•β•β•β•šβ•β• β•šβ•β• β•šβ•β• β•šβ•β• β•šβ•β•β•β•β•β• "); - tui.append_line("[sys] ─────────── agentic LLM shell v1.0 ────────────"); + tui.append_line("[logo_0] β–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•—β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— "); + tui.append_line("[logo_1] β–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘β•šβ•β•β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•—"); + tui.append_line("[logo_2] β–ˆβ–ˆβ•”β–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘"); + tui.append_line("[logo_3] β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘"); + tui.append_line("[logo_4] β–ˆβ–ˆβ•‘ β•šβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•"); + tui.append_line("[logo_5] β•šβ•β• β•šβ•β•β•β•β•šβ•β• β•šβ•β• β•šβ•β• β•šβ•β• β•šβ•β•β•β•β•β• "); + tui.append_line("[logo_6] ─────────── agentic LLM shell v1.0 ──────────────"); tui.append_line(""); tui.append_line("[sys] Sandbox : " + sandbox); tui.append_line("[sys] /help for commands Β· exit to quit"); @@ -1798,26 +2056,26 @@ int main(int argc, char **argv) { cfg.n_gpu_layers = std::stoi(take_next(a.c_str())); } else if (a == "-h" || a == "--help") { std::puts( - "Usage: nitro [options] [project_dir]\n" - "\n" - "Options:\n" - " -m, --model GGUF model to load on startup\n" - " -e, --embed embedding model for RAG\n" - " -g, --gpu-layers GPU layers to offload (default: 32)\n" - " -h, --help show this help\n" - "\n" - "project_dir defaults to the current working directory.\n" - "Settings are persisted to ~/.config/nitro.settings.json.\n" - "\n" - "Slash commands inside nitro:\n" - " /model load / hot-reload a GGUF\n" - " /embed load an embedding model\n" - " /rag [path] index file or directory (picker if no path)\n" - " /memory KV / VRAM / layer stats\n" - " /settings show current settings\n" - " /clear reset conversation\n" - " /help list commands\n" - ); + "Usage: nitro [options] [project_dir]\n" + "\n" + "Options:\n" + " -m, --model GGUF model to load on startup\n" + " -e, --embed embedding model for RAG\n" + " -g, --gpu-layers GPU layers to offload (default: 32)\n" + " -h, --help show this help\n" + "\n" + "project_dir defaults to the current working directory.\n" + "Settings are persisted to ~/.config/nitro.settings.json.\n" + "\n" + "Slash commands inside nitro:\n" + " /model load / hot-reload a GGUF\n" + " /embed load an embedding model\n" + " /rag [path] index file or directory (picker if no path)\n" + " /memory KV / VRAM / layer stats\n" + " /settings show current settings\n" + " /clear reset conversation\n" + " /help list commands\n" + ); return 0; } else if (!a.empty() && a[0] == '-') { std::fprintf(stderr, "nitro: unknown option '%s' (try --help)\n", a.c_str()); @@ -1845,6 +2103,8 @@ int main(int argc, char **argv) { // ── Init TUI ────────────────────────────────────────────────────── TuiState tui; tui.init(); + // Load persisted input history so up-arrow works across sessions. + tui.history.load(history_path()); welcome(tui, cfg.sandbox); // ── Init agent ──────────────────────────────────────────────────── @@ -1886,6 +2146,8 @@ int main(int argc, char **argv) { } tui.destroy(); + // Persist input history for the next session. + tui.history.save(history_path()); curl_global_cleanup(); return 0; } From 87981a1acb9488932d3539af1c7fea61c08b4580 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Fri, 22 May 2026 14:39:48 +0930 Subject: [PATCH 113/131] LLAMA: nitro agent - wip --- llama/nitro.cpp | 263 +++++++++++++++++++++++++++++------------------- 1 file changed, 160 insertions(+), 103 deletions(-) diff --git a/llama/nitro.cpp b/llama/nitro.cpp index 9a7424f..b35e10e 100644 --- a/llama/nitro.cpp +++ b/llama/nitro.cpp @@ -18,8 +18,8 @@ // -g, --gpu-layers layers to offload to GPU (default: 32) // // Slash commands: -// /model β€” load / hot-reload a GGUF model -// /embed β€” load an embedding model for RAG +// /model β€” load / hot-reload a GGUF model (picker if no path) +// /embed β€” load an embedding model for RAG (picker if no path) // /rag β€” index a file or directory into RAG // /memory β€” show KV / VRAM / layer stats // /clear β€” reset conversation (keeps system prompt) @@ -494,13 +494,23 @@ struct TuiState { // Presents an interactive directory browser to let the user choose a // folder (or file) to index. Returns the selected path, or empty string // if the user cancelled. - std::string rag_folder_picker(const std::string &start_dir); + // ── RAG / file browser popup ───────────────────────────────────── + // Used by /rag, /model, and /embed to pick a path interactively. + // Pass a hint string shown in the title bar (e.g. "RAG Folder", + // "Model File", "Embedding Model"). + // Returns the selected path, or empty string if the user cancelled. + std::string file_picker(const std::string &start_dir, + const std::string &title_hint = "File"); + // Legacy alias kept for callers that used the old name. + std::string rag_folder_picker(const std::string &start_dir) { + return file_picker(start_dir, "RAG Folder"); + } }; + // ─── colour helpers ────────────────────────────────────────────────────── static constexpr uint32_t BG_CHAT_R = 18, BG_CHAT_G = 22, BG_CHAT_B = 30; static constexpr uint32_t BG_INP_R = 22, BG_INP_G = 28, BG_INP_B = 38; static constexpr uint32_t BG_HDR_R = 30, BG_HDR_G = 40, BG_HDR_B = 55; - static inline uint64_t fg_rgb(uint32_t r, uint32_t g, uint32_t b) { return NCCHANNELS_INITIALIZER(r, g, b, 0, 0, 0); } @@ -513,6 +523,7 @@ static inline uint64_t inp_ch(uint32_t r, uint32_t g, uint32_t b) { static inline uint64_t hdr_ch(uint32_t r, uint32_t g, uint32_t b) { return NCCHANNELS_INITIALIZER(r, g, b, BG_HDR_R, BG_HDR_G, BG_HDR_B); } + // ─── TuiState::init ────────────────────────────────────────────────────── void TuiState::init() { notcurses_options opts{}; @@ -544,13 +555,12 @@ void TuiState::init() { ncplane_set_base(inputpl, " ", 0, NCCHANNELS_INITIALIZER(BG_INP_R, BG_INP_G, BG_INP_B, BG_INP_R, BG_INP_G, BG_INP_B)); + notcurses_mice_enable(nc, NCMICE_ALL_EVENTS); redraw_all(); } - void TuiState::destroy() { if (nc) { notcurses_stop(nc); nc = nullptr; } } - void TuiState::resize() { notcurses_term_dim_yx(nc, (unsigned *)&term_rows, (unsigned *)&term_cols); ncplane_resize_simple(header, 1, (unsigned)term_cols); @@ -787,23 +797,33 @@ void TuiState::dismiss_modal_popup() { // Keyboard: ↑/↓ navigate, Enter select/descend, Backspace go up, // 's' select current dir for indexing, Esc cancel. // Returns the chosen path or "" on cancel. -std::string TuiState::rag_folder_picker(const std::string &start_dir) { +// ─── TuiState::file_picker ──────────────────────────────────────────────── +// Unified interactive directory/file browser used by /rag, /model, /embed. +// title_hint appears in the popup header (e.g. "RAG Folder", "Model File"). +// +// Keyboard: +// ↑/↓ navigate list +// Enter descend into directory, or select a file +// Backspace go up one directory +// s select the current directory itself (useful for /rag) +// Esc cancel β†’ returns "" +// +// Returns the chosen path, or "" on cancel. +std::string TuiState::file_picker(const std::string &start_dir, + const std::string &title_hint) { std::string current_dir = start_dir; { std::error_code ec; auto canon = fs::canonical(start_dir, ec); if (!ec) current_dir = canon.string(); } - - // Build an entry list for the current directory. auto load_entries = [](const std::string &dir, std::vector &entries) { entries.clear(); std::error_code ec; - // Add ".." for going up (except at fs root). - if (fs::path(dir).has_parent_path() && fs::path(dir) != fs::path(dir).root_path()) + if (fs::path(dir).has_parent_path() && + fs::path(dir) != fs::path(dir).root_path()) entries.push_back(".."); - // Dirs first, then files. std::vector dirs, files; for (const auto &e : fs::directory_iterator(dir, ec)) { if (ec) break; @@ -837,12 +857,17 @@ std::string TuiState::rag_folder_picker(const std::string &start_dir) { static constexpr uint32_t PBG_R = 18, PBG_G = 24, PBG_B = 40; ncplane_set_base(picker, " ", 0, NCCHANNELS_INITIALIZER(PBG_R, PBG_G, PBG_B, PBG_R, PBG_G, PBG_B)); - + // Build a compact hint line appropriate to the operation. + // /rag adds 's=select dir'; /model and /embed only need file selection. + std::string hint_line = "↑↓ navigate Enter open/select Esc cancel"; + if (title_hint.find("RAG") != std::string::npos || + title_hint.find("Folder") != std::string::npos) { + hint_line = "↑↓ navigate Enter open s=select dir Esc cancel"; + } auto draw_picker = [&]() { ncplane_erase(picker); uint64_t border_ch = NCCHANNELS_INITIALIZER(100, 180, 255, PBG_R, PBG_G, PBG_B); ncplane_set_channels(picker, border_ch); - // Border ncplane_putstr_yx(picker, 0, 0, "β•”"); for (int c = 1; c < PW - 1; ++c) ncplane_putstr_yx(picker, 0, c, "═"); ncplane_putstr_yx(picker, 0, PW - 1, "β•—"); @@ -857,43 +882,40 @@ std::string TuiState::rag_folder_picker(const std::string &start_dir) { // Title ncplane_set_channels(picker, NCCHANNELS_INITIALIZER(255, 220, 80, PBG_R, PBG_G, PBG_B)); - ncplane_putstr_yx(picker, 0, 2, " πŸ“‚ RAG Folder Picker "); - - // Current path (truncated to fit) + std::string title_str = " πŸ“‚ " + title_hint + " Picker "; + if ((int)title_str.size() > PW - 4) title_str = title_str.substr(0, PW - 4); + ncplane_putstr_yx(picker, 0, 2, title_str.c_str()); + // Current path (truncated). std::string path_display = current_dir; if ((int)path_display.size() > PW - 4) path_display = "…" + path_display.substr(path_display.size() - (PW - 5)); ncplane_set_channels(picker, NCCHANNELS_INITIALIZER(160, 200, 240, PBG_R, PBG_G, PBG_B)); ncplane_putstr_yx(picker, 1, 2, path_display.c_str()); - - // Hint line + // Hint line (bottom interior row). ncplane_set_channels(picker, NCCHANNELS_INITIALIZER(120, 120, 160, PBG_R, PBG_G, PBG_B)); - ncplane_putstr_yx(picker, PH - 2, 2, - "↑↓ navigate Enter open s=select dir Esc cancel"); - - // Entry list - int list_rows = PH - 5; // rows 2 … PH-4 available - // Clamp scroll so selected stays visible + std::string hint_trunc = hint_line; + if ((int)hint_trunc.size() > PW - 4) hint_trunc = hint_trunc.substr(0, PW - 4); + ncplane_putstr_yx(picker, PH - 2, 2, hint_trunc.c_str()); + // Entry list. + int list_rows = PH - 5; if (selected < scroll) scroll = selected; if (selected >= scroll + list_rows) scroll = selected - list_rows + 1; - for (int i = 0; i < list_rows; ++i) { int idx = scroll + i; if (idx >= (int)entries.size()) break; bool is_selected = (idx == selected); bool is_dir = !entries[idx].empty() && entries[idx].back() == '/'; uint32_t fr, fg, fb; - if (is_selected) { fr = 20; fg = 20; fb = 20; } - else if (is_dir) { fr = 120; fg = 200; fb = 255; } - else { fr = 200; fg = 200; fb = 200; } + if (is_selected) { fr = 20; fg = 20; fb = 20; } + else if (is_dir) { fr = 120; fg = 200; fb = 255; } + else { fr = 200; fg = 200; fb = 200; } uint32_t br = is_selected ? 100 : PBG_R; uint32_t bg = is_selected ? 180 : PBG_G; uint32_t bb = is_selected ? 255 : PBG_B; ncplane_set_channels(picker, NCCHANNELS_INITIALIZER(fr, fg, fb, br, bg, bb)); - // Pad entry to fill width std::string label = (is_selected ? " β–Ά " : " ") + entries[idx]; if ((int)label.size() > PW - 2) label = label.substr(0, PW - 2); while ((int)label.size() < PW - 2) label += ' '; @@ -922,6 +944,7 @@ std::string TuiState::rag_folder_picker(const std::string &start_dir) { draw_picker(); continue; } + // 's' β€” select the current directory (useful for /rag, ignored for file pickers). if (ni.id == 's' || ni.id == 'S') { // Select current directory for RAG indexing. result = current_dir; @@ -962,13 +985,13 @@ std::string TuiState::rag_folder_picker(const std::string &start_dir) { draw_picker(); } else { // Select a specific file. + // Select the highlighted file. result = current_dir + "/" + entry; break; } continue; } } - ncplane_destroy(picker); notcurses_render(nc); return result; @@ -1029,6 +1052,7 @@ std::string TuiState::readline_blocking() { return result; } + if (ni.id == NCKEY_UP) { // Entering history from a fresh prompt: save current text as draft. std::string hist_entry; @@ -1077,7 +1101,18 @@ std::string TuiState::readline_blocking() { notcurses_render(nc); continue; } - + if (ni.id == NCKEY_SCROLL_UP && scroll_offset < term_rows + 10) { + scroll_offset += 1; + redraw_chat(); + notcurses_render(nc); + continue; + } + if (ni.id == NCKEY_SCROLL_DOWN && scroll_offset > 0) { + scroll_offset -= 1; + redraw_chat(); + notcurses_render(nc); + continue; + } if (ni.id == NCKEY_BACKSPACE || ni.id == 127) { if (cursor_pos > 0) { input_buf.erase(cursor_pos - 1, 1); --cursor_pos; } } else if (ni.id == NCKEY_LEFT) { @@ -1108,7 +1143,7 @@ std::string TuiState::readline_blocking() { // AgentState // ═══════════════════════════════════════════════════════════════════════════ struct AgentState { - Llama llama; + std::unique_ptr llama; std::unique_ptr iter; std::unique_ptr embed_llama; std::unique_ptr rag_db; @@ -1127,18 +1162,17 @@ struct AgentState { std::string memory_info_text(); float tokens_per_sec() const; }; - void AgentState::apply_generation_params(const NitroConfig &cfg) { - llama.add_stop("<|turn|>"); - llama.add_stop("<|im_end|>"); - llama.set_max_tokens(cfg.n_max_tokens); - llama.set_temperature(cfg.temperature); - llama.set_top_k(cfg.top_k); - llama.set_top_p(cfg.top_p); - llama.set_min_p(cfg.min_p); - llama.set_penalty_repeat(cfg.penalty_repeat); - llama.set_penalty_last_n(cfg.penalty_last_n); - llama.set_log_level(cfg.log_level); + llama->add_stop("<|turn|>"); + llama->add_stop("<|im_end|>"); + llama->set_max_tokens(cfg.n_max_tokens); + llama->set_temperature(cfg.temperature); + llama->set_top_k(cfg.top_k); + llama->set_top_p(cfg.top_p); + llama->set_min_p(cfg.min_p); + llama->set_penalty_repeat(cfg.penalty_repeat); + llama->set_penalty_last_n(cfg.penalty_last_n); + llama->set_log_level(cfg.log_level); } // ─── AgentState::setup_model ────────────────────────────────────────────── @@ -1152,22 +1186,25 @@ bool AgentState::setup_model(const NitroConfig &cfg, TuiState &tui) { // Show a modal popup so the user knows loading is in progress. std::string model_name = fs::path(cfg.model_path).filename().string(); tui.show_modal_popup("Loading " + model_name); + // Destroy the iterator first β€” it holds references into the llama context. + // Freeing llama while iter is still alive causes use-after-free / load failure. + iter.reset(); + model_loaded = false; + llama = std::make_unique(); - llama.reset(); apply_generation_params(cfg); - if (!llama.load_model(cfg.model_path, cfg.n_ctx, cfg.n_batch, - cfg.n_gpu_layers, cfg.log_level)) { + if (!llama->load_model(cfg.model_path, cfg.n_ctx, cfg.n_batch, + cfg.n_gpu_layers, cfg.log_level)) { tui.dismiss_modal_popup(); - tui.append_line(std::string("[err] ") + llama.last_error()); + tui.append_line(std::string("[err] ") + llama->last_error()); tui.redraw_all(); return false; } tui.dismiss_modal_popup(); - model_loaded = true; tui.current_model = model_name; tui.append_line("[sys] Model ready: " + tui.current_model); - LlamaMemoryInfo mem = llama.memory_info(); + LlamaMemoryInfo mem = llama->memory_info(); tui.append_line("[sys] " + mem.advice); tui.kv_used = mem.kv_used; tui.kv_total = mem.kv_total; @@ -1198,11 +1235,11 @@ bool AgentState::setup_embed(const std::string &path, TuiState &tui) { void AgentState::reset_conversation(const std::string &sysprompt, TuiState &tui) { system_prompt = sysprompt; - llama.reset(); + llama->reset(); apply_generation_params(NitroConfig{}); iter = std::make_unique(); - if (!llama.add_message(*iter, "system", system_prompt)) { - tui.append_line(std::string("[err] System prompt injection: ") + llama.last_error()); + if (!llama->add_message(*iter, "system", system_prompt)) { + tui.append_line(std::string("[err] System prompt injection: ") + llama->last_error()); tui.redraw_all(); } } @@ -1217,7 +1254,7 @@ float AgentState::tokens_per_sec() const { std::string AgentState::memory_info_text() { if (!model_loaded) return "No model loaded."; - LlamaMemoryInfo m = llama.memory_info(); + LlamaMemoryInfo m = llama->memory_info(); std::ostringstream oss; oss << "KV cache : " << m.kv_used << " / " << m.kv_total << " (" << m.kv_percent << "%)\n"; @@ -1270,8 +1307,8 @@ bool AgentState::run_turn(const std::string &user_message, } std::string effective_message = user_message; if (embed_llama && rag_db && rag_session) { - std::string context = llama.rag_retrieve(*rag_db, user_message, - cfg.rag_top_k, *rag_session); + std::string context = llama->rag_retrieve(*rag_db, user_message, + cfg.rag_top_k, *rag_session); if (!context.empty()) { effective_message = "Context:\n" + context + "\n\nUser: " + user_message; } @@ -1281,24 +1318,21 @@ bool AgentState::run_turn(const std::string &user_message, tui.redraw_all(); return false; } - if (!llama.add_message(*iter, "user", effective_message)) { - tui.append_line(std::string("[err] add_message: ") + llama.last_error()); + if (!llama->add_message(*iter, "user", effective_message)) { + tui.append_line(std::string("[err] add_message: ") + llama->last_error()); tui.redraw_all(); return false; } tui.append_line("Nitro: "); tui.set_thinking(true); - bool in_think = true; std::string buffer; - auto update_think_state = [&](const std::string &text) { if (text.find("") != std::string::npos || text.find("<|think|>") != std::string::npos) in_think = true; if (text.find("") != std::string::npos || text.find("") != std::string::npos) in_think = false; }; - auto remove_substr = [](std::string str, const std::string& toRemove) { size_t pos = str.find(toRemove); while (pos != std::string::npos) { @@ -1309,7 +1343,7 @@ bool AgentState::run_turn(const std::string &user_message, }; while (iter->_has_next) { - std::string tok = llama.next(*iter); + std::string tok = llama->next(*iter); tui.tick_spinner(); update_think_state(tok); buffer += tok; @@ -1320,7 +1354,7 @@ bool AgentState::run_turn(const std::string &user_message, std::string trimmed = text_line; trimmed.erase(0, trimmed.find_first_not_of(" \t")); if (trimmed.substr(0, 5) == "TOOL:") { - std::string tail = buffer + llama.all(*iter); + std::string tail = buffer + llama->all(*iter); std::string op, arg1, payload; { auto s1 = trimmed.find(' '); @@ -1361,8 +1395,14 @@ bool AgentState::run_turn(const std::string &user_message, tui.append_line("[tool] β†’ " + result.substr(0, 200) + (result.size() > 200 ? "…" : "")); tui.redraw_all(); - if (!llama.add_message(*iter, "tool", result)) { - tui.append_line(std::string("[err] tool result inject: ") + llama.last_error()); + // Inject the tool result back into the conversation as a user-role + // message using the TOOL_RESULT: prefix that the system prompt + // describes. Using "user" role ensures the injected text is correctly + // formatted regardless of whether the underlying llama-sb layer knows + // a dedicated "tool" role. + std::string tool_result_msg = "TOOL_RESULT: " + result; + if (!llama->add_message(*iter, "user", tool_result_msg)) { + tui.append_line(std::string("[err] tool result inject: ") + llama->last_error()); tui.redraw_all(); break; } @@ -1382,22 +1422,20 @@ bool AgentState::run_turn(const std::string &user_message, std::string result = process_tool(trimmed, cfg.sandbox, cfg.run_allowed, tui); tui.append_line("[tool] β†’ " + result.substr(0, 200)); tui.redraw_all(); - llama.add_message(*iter, "tool", result); + std::string tool_result_msg = "TOOL_RESULT: " + result; + llama->add_message(*iter, "user", tool_result_msg); } else if (!in_think) { tui.append_token(buffer); } } - tui.flush_token_acc(); tui.set_thinking(false); - tui.tokens_per_sec = tokens_per_sec(); - LlamaMemoryInfo mem = llama.memory_info(); + LlamaMemoryInfo mem = llama->memory_info(); tui.kv_used = mem.kv_used; tui.kv_total = mem.kv_total; tui.vram_used = mem.vram_used; tui.vram_total = mem.vram_total; - char stat[128]; std::snprintf(stat, sizeof(stat), "[sys] %.1f tok/s (%d tokens) KV %.1f%%", (double)tui.tokens_per_sec, @@ -1613,35 +1651,30 @@ static std::string html_to_text(const std::string &html) { } // ═══════════════════════════════════════════════════════════════════════════ -// TOOL:CURL β€” HTTP GET with libcurl, returns body text (capped at 32 KB). +// TOOL:CURL // ═══════════════════════════════════════════════════════════════════════════ static size_t curl_write_cb(void *contents, size_t size, size_t nmemb, void *userp) { std::string *buf = static_cast(userp); size_t total = size * nmemb; - // Enforce a 32 KB cap to prevent flooding the context window. static constexpr size_t MAX_BODY = 32 * 1024; if (buf->size() < MAX_BODY) { size_t room = MAX_BODY - buf->size(); buf->append(static_cast(contents), std::min(total, room)); } - return total; // Return full amount so curl doesn't abort. + return total; } - static std::string tool_curl(const std::string &url) { if (url.empty()) return "ERROR: TOOL:CURL requires a URL argument"; - CURL *curl = curl_easy_init(); if (!curl) return "ERROR: curl_easy_init failed"; - std::string body; body.reserve(4096); - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_cb); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &body); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5L); - curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L); // 15-second timeout + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L); curl_easy_setopt(curl, CURLOPT_USERAGENT, "nitro/1.0"); // Accept compressed responses; curl will decompress automatically. curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, ""); @@ -1656,9 +1689,7 @@ static std::string tool_curl(const std::string &url) { std::string content_type = ct_raw ? ct_raw : ""; std::transform(content_type.begin(), content_type.end(), content_type.begin(), ::tolower); - curl_easy_cleanup(curl); - if (res != CURLE_OK) { return std::string("ERROR: curl: ") + curl_easy_strerror(res); } @@ -1748,13 +1779,11 @@ static std::string process_tool(const std::string &cmd, return result; } if (op == "TOOL:CURL") { - // arg1 holds the URL (no sandbox restriction β€” network, not filesystem). return tool_curl(arg1); } if (op == "TOOL:RUN") { std::string prog = resolve(arg1); if (!path_in_sandbox(sandbox, prog)) return "ERROR: path outside sandbox"; - // Enforce allowlist if one is configured. if (!run_allowed.empty()) { std::string basename = fs::path(prog).filename().string(); bool permitted = std::any_of(run_allowed.begin(), run_allowed.end(), @@ -1788,7 +1817,7 @@ static std::string build_system_prompt(const std::vector &knowledge "## Tool protocol\n" "Emit tool calls on their own line. The host executes them and returns\n" "TOOL_RESULT: on the next line.\n\n" - "Available tools:\n" + "## Available tools:\n" " TOOL:LIST [dir] list files (default: sandbox root)\n" " TOOL:READ read file contents\n" " TOOL:WRITE write text to file\n" @@ -1799,12 +1828,25 @@ static std::string build_system_prompt(const std::vector &knowledge " TOOL:RND random float\n" " TOOL:PERMISSION ask user for explicit permission\n" " TOOL:CURL HTTP GET; returns response body (max 32 KB)\n\n" - "Rules:\n" + "## Rules:\n" "- Never access files outside the sandbox.\n" - "- Use TOOL:PERMISSION before destructive or irreversible operations.\n" + "- Use TOOL:PERMISSION when you're about to modify files, delete data, or run external programs.\n" "- Use TOOL:CURL to fetch documentation, APIs, or web content you need.\n" "- Reason step-by-step inside <|think|>… (hidden from user).\n" - "- After each tool call, explain what you did in plain English.\n\n"; + "- After each tool call, explain what you did in plain English.\n" + "Ask the user for explicit permission before proceeding.\n\n" + "## File Reading\n" + "When you read a file with TOOL:READ, you MUST:\n" + "1. Acknowledge what you found\n" + "2. Use that information in your response\n" + "3. If the file contains code, explain it or show relevant parts\n" + "4. If the file contains documentation, summarize key points\n" + "## Tool Result Integration\n" + "When you see TOOL_RESULT in the conversation, it contains tool output.\n" + "Use it to:\n" + "- Answer questions based on file contents\n" + "- Explain code or configuration\n" + "- Provide accurate information from the file\n"; for (const auto &kf : knowledge_files) { std::ifstream f(kf); if (!f) continue; @@ -1831,8 +1873,8 @@ static void handle_slash(const std::string &input, if (verb == "/help") { tui.append_line("[sys] Commands:"); - tui.append_line("[sys] /model load a GGUF model"); - tui.append_line("[sys] /embed load an embedding model for RAG"); + tui.append_line("[sys] /model [path] load a GGUF model (picker if no path)"); + tui.append_line("[sys] /embed [path] load an embedding model (picker if no path)"); tui.append_line("[sys] /rag [path] index file or directory (picker if no path)"); tui.append_line("[sys] /memory KV / VRAM / layer stats"); tui.append_line("[sys] /clear reset conversation"); @@ -1847,11 +1889,19 @@ static void handle_slash(const std::string &input, tui.redraw_all(); return; } - + // ── /model ────────────────────────────────────────────────────────────── + // If no path is given, open the file picker so the user can browse to a + // GGUF. The picker starts in the current sandbox directory. if (verb == "/model") { if (rest.empty()) { - tui.append_line("[err] Usage: /model "); - tui.redraw_all(); return; + tui.append_line("[sys] Opening model picker…"); + tui.redraw_all(); + rest = tui.file_picker(cfg.sandbox, "Model File"); + if (rest.empty()) { + tui.append_line("[sys] /model cancelled."); + tui.redraw_all(); + return; + } } cfg.model_path = rest; if (agent.setup_model(cfg, tui)) { @@ -1863,10 +1913,19 @@ static void handle_slash(const std::string &input, return; } + // ── /embed ────────────────────────────────────────────────────────────── + // If no path is given, open the file picker so the user can browse to an + // embedding GGUF. if (verb == "/embed") { if (rest.empty()) { - tui.append_line("[err] Usage: /embed "); - tui.redraw_all(); return; + tui.append_line("[sys] Opening embedding model picker…"); + tui.redraw_all(); + rest = tui.file_picker(cfg.sandbox, "Embedding Model"); + if (rest.empty()) { + tui.append_line("[sys] /embed cancelled."); + tui.redraw_all(); + return; + } } cfg.embed_path = rest; if (agent.setup_embed(rest, tui)) { @@ -1875,6 +1934,7 @@ static void handle_slash(const std::string &input, return; } + // ── /rag ──────────────────────────────────────────────────────────────── if (verb == "/rag") { std::string path = rest; if (path.empty()) { @@ -1943,8 +2003,7 @@ static void handle_slash(const std::string &input, } bool ok = true; - bool needs_reparam = false; // whether to re-apply generation params - + bool needs_reparam = false; try { if (key == "temperature") { cfg.temperature = std::stof(val); needs_reparam = true; } else if (key == "top_p") { cfg.top_p = std::stof(val); needs_reparam = true; } @@ -2002,8 +2061,6 @@ static void handle_slash(const std::string &input, // Welcome banner β€” colourful multi-line ASCII logo // ═══════════════════════════════════════════════════════════════════════════ static void welcome(TuiState &tui, const std::string &sandbox) { - // Logo lines use the "[logo_N]" prefix so redraw_chat applies a - // per-row cyanβ†’magenta gradient (N = 0-6 maps to the gradient table). tui.append_line(""); tui.append_line("[logo_0] β–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•—β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— "); tui.append_line("[logo_1] β–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘β•šβ•β•β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•—"); @@ -2025,9 +2082,8 @@ static void welcome(TuiState &tui, const std::string &sandbox) { int main(int argc, char **argv) { // ── Load persisted settings first (provides defaults) ──────────── NitroConfig cfg; - load_settings(cfg); // silently no-ops if ~/.config/nitro.settings.json absent - // ── Parse arguments (command-line overrides saved settings) ────── + load_settings(cfg); auto resolve_path = [](const std::string &arg) -> std::string { std::error_code ec; if (arg.substr(0, 2) == "~/") { @@ -2068,9 +2124,9 @@ int main(int argc, char **argv) { "Settings are persisted to ~/.config/nitro.settings.json.\n" "\n" "Slash commands inside nitro:\n" - " /model load / hot-reload a GGUF\n" - " /embed load an embedding model\n" - " /rag [path] index file or directory (picker if no path)\n" + " /model [path] load / hot-reload a GGUF (picker if no path)\n" + " /embed [path] load an embedding model (picker if no path)\n" + " /rag [path] index file or directory (picker if no path)\n" " /memory KV / VRAM / layer stats\n" " /settings show current settings\n" " /clear reset conversation\n" @@ -2117,7 +2173,8 @@ int main(int argc, char **argv) { if (!cfg.embed_path.empty()) agent.setup_embed(cfg.embed_path, tui); } else { - tui.append_line("[sys] No model specified. Use /model to load one."); + tui.append_line("[sys] No model specified. Use /model to open the file picker,"); + tui.append_line("[sys] or /model to load directly."); tui.append_line("[sys] Example: /model ~/models/qwen2.5-7b-q4_k_m.gguf"); tui.redraw_all(); } From 2a46e0955de12067fecdbcd2c53b505b2f32a48f Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Sat, 23 May 2026 19:04:58 +0930 Subject: [PATCH 114/131] LLAMA: nitro agent - wip --- llama/nitro.cpp | 169 +++++++++++++++++++++++++++++------------------- 1 file changed, 104 insertions(+), 65 deletions(-) diff --git a/llama/nitro.cpp b/llama/nitro.cpp index b35e10e..ece71cb 100644 --- a/llama/nitro.cpp +++ b/llama/nitro.cpp @@ -38,7 +38,8 @@ // TOOL:CURL // // Copyright (C) 2026 Chris Warren-Smith β€” GPLv2 or later -// ─── Standard library ──────────────────────────────────────────────────────── +// + #include #include #include @@ -50,14 +51,13 @@ #include #include #include -// ─── curl ───────────────────────────────────────────────────────────────────── #include -// ─── Integration layer (sole llama.cpp dependency for nitro) ───────────────── #include "llama-sb.h" #include "llama-sb-rag.h" -// ─── TUI ───────────────────────────────────────────────────────────────────── #include + namespace fs = std::filesystem; + // ═══════════════════════════════════════════════════════════════════════════ // Forward declarations // ═══════════════════════════════════════════════════════════════════════════ @@ -75,6 +75,7 @@ static std::string process_tool(const std::string &line, const std::string &san TuiState &tui); static std::string build_system_prompt(const std::vector &knowledge_files, const std::string &sandbox); + // ─── RAG indexing ───────────────────────────────────────────────────────────── static constexpr int BATCH_SIZE = 512; @@ -448,6 +449,7 @@ struct TuiState { // ── input ───────────────────────────────────────────────────────── std::string input_buf; size_t cursor_pos = 0; + bool mouse_mode = true; // ── status bar values ───────────────────────────────────────────── std::string current_model = "none"; float tokens_per_sec = 0.0f; @@ -489,6 +491,7 @@ struct TuiState { // handle β€” or just use the paired helpers below. struct ncplane *modal_plane = nullptr; void show_modal_popup(const std::string &message); + void show_help(); void dismiss_modal_popup(); // ── RAG folder picker popup ─────────────────────────────────────── // Presents an interactive directory browser to let the user choose a @@ -555,7 +558,7 @@ void TuiState::init() { ncplane_set_base(inputpl, " ", 0, NCCHANNELS_INITIALIZER(BG_INP_R, BG_INP_G, BG_INP_B, BG_INP_R, BG_INP_G, BG_INP_B)); - notcurses_mice_enable(nc, NCMICE_ALL_EVENTS); + notcurses_mice_enable(nc, NCMICE_BUTTON_EVENT); redraw_all(); } void TuiState::destroy() { @@ -784,6 +787,24 @@ void TuiState::show_modal_popup(const std::string &message) { notcurses_render(nc); } +void TuiState::show_help() { + append_line("[sys] Commands:"); + append_line("[sys] /model [path] load a GGUF model (picker if no path)"); + append_line("[sys] /embed [path] load an embedding model (picker if no path)"); + append_line("[sys] /rag [path] index file or directory (picker if no path)"); + append_line("[sys] /memory KV / VRAM / layer stats"); + append_line("[sys] /clear reset conversation"); + append_line("[sys] /settings show current settings"); + append_line("[sys] /set change a setting live"); + append_line("[sys] /help this message"); + append_line("[sys] exit / quit exit Nitro"); + append_line("[sys] Settable keys (via /set):"); + append_line("[sys] temperature top_p top_k min_p penalty_repeat"); + append_line("[sys] n_max_tokens penalty_last_n rag_top_k n_gpu_layers"); + append_line("[sys] run_allowed (comma-separated list, e.g. python3,make)"); + redraw_all(); +} + void TuiState::dismiss_modal_popup() { if (modal_plane) { ncplane_destroy(modal_plane); @@ -1052,7 +1073,6 @@ std::string TuiState::readline_blocking() { return result; } - if (ni.id == NCKEY_UP) { // Entering history from a fresh prompt: save current text as draft. std::string hist_entry; @@ -1107,12 +1127,25 @@ std::string TuiState::readline_blocking() { notcurses_render(nc); continue; } - if (ni.id == NCKEY_SCROLL_DOWN && scroll_offset > 0) { + if (ni.id == NCKEY_SCROLL_DOWN && scroll_offset > 1) { scroll_offset -= 1; redraw_chat(); notcurses_render(nc); continue; } + if (ni.id == NCKEY_F01) { + show_help(); + continue; + } + if (ni.id == NCKEY_F02) { + mouse_mode = !mouse_mode; + if (mouse_mode) { + notcurses_mice_enable(nc, NCMICE_BUTTON_EVENT); + } else { + notcurses_mice_disable(nc); + } + continue; + } if (ni.id == NCKEY_BACKSPACE || ni.id == 127) { if (cursor_pos > 0) { input_buf.erase(cursor_pos - 1, 1); --cursor_pos; } } else if (ni.id == NCKEY_LEFT) { @@ -1294,6 +1327,40 @@ bool AgentState::rag_index(const std::string &path, TuiState &tui) { return true; } +// ═══════════════════════════════════════════════════════════════════════════ +// Think-tag filtering +// ═══════════════════════════════════════════════════════════════════════════ +// Strips everything between (and including) … or the +// <|think|>… variant from a completed line/buffer. +// Also strips any bare close-tag that appears without a matching open-tag +// (can happen when the open was in a previous chunk already consumed). +// Returns the visible text that should be shown to the user. +std::string filter_think_tags(const std::string &text) { + static const struct { const char *open; const char *close; } PAIRS[] = { + { "", "" }, + { "<|think|>", "" }, + }; + std::string out = text; + for (auto &p : PAIRS) { + std::string open(p.open), close(p.close); + for (;;) { + auto ob = out.find(open); + if (ob == std::string::npos) break; + auto ce = out.find(close, ob); + if (ce == std::string::npos) { + out.erase(ob); // no closing tag β€” strip to end + break; + } + out.erase(ob, ce + close.size() - ob); + } + // Strip orphan close-tags (open tag was in an earlier chunk). + size_t pos = 0; + while ((pos = out.find(close, pos)) != std::string::npos) + out.erase(pos, close.size()); + } + return out; +} + // ═══════════════════════════════════════════════════════════════════════════ // Agent turn // ═══════════════════════════════════════════════════════════════════════════ @@ -1324,29 +1391,32 @@ bool AgentState::run_turn(const std::string &user_message, return false; } tui.append_line("Nitro: "); - tui.set_thinking(true); - bool in_think = true; + // in_think starts false β€” models that don't use blocks emit + // visible text immediately. The spinner activates only while thinking. + bool in_think = false; + tui.set_thinking(false); std::string buffer; - auto update_think_state = [&](const std::string &text) { - if (text.find("") != std::string::npos || - text.find("<|think|>") != std::string::npos) in_think = true; - if (text.find("") != std::string::npos || - text.find("") != std::string::npos) in_think = false; - }; - auto remove_substr = [](std::string str, const std::string& toRemove) { - size_t pos = str.find(toRemove); - while (pos != std::string::npos) { - str.erase(pos, toRemove.length()); - pos = str.find(toRemove, pos); - } - return str; - }; - while (iter->_has_next) { std::string tok = llama->next(*iter); - tui.tick_spinner(); - update_think_state(tok); buffer += tok; + // Detect think-tag transitions on the accumulated buffer so we never + // miss a tag that was split across two tokens. + bool was_thinking = in_think; + if (!in_think) { + if (buffer.find("") != std::string::npos || + buffer.find("<|think|>") != std::string::npos) + in_think = true; + } + if (in_think) { + if (buffer.find("") != std::string::npos || + buffer.find("") != std::string::npos) + in_think = false; + } + // Update spinner: animate only while in a think block. + if (in_think || was_thinking) { + tui.set_thinking(in_think); + if (in_think) tui.tick_spinner(); + } auto nl = buffer.find('\n'); if (nl != std::string::npos) { std::string text_line = buffer.substr(0, nl); @@ -1408,13 +1478,10 @@ bool AgentState::run_turn(const std::string &user_message, } buffer.clear(); } else if (!in_think) { - text_line = remove_substr(text_line, ""); - text_line = remove_substr(text_line, ""); - tui.append_token(text_line + "\n"); + tui.append_token(filter_think_tags(text_line) + "\n"); } } } - if (!buffer.empty()) { std::string trimmed = buffer; trimmed.erase(0, trimmed.find_first_not_of(" \t")); @@ -1425,7 +1492,7 @@ bool AgentState::run_turn(const std::string &user_message, std::string tool_result_msg = "TOOL_RESULT: " + result; llama->add_message(*iter, "user", tool_result_msg); } else if (!in_think) { - tui.append_token(buffer); + tui.append_token(filter_think_tags(buffer)); } } tui.flush_token_acc(); @@ -1817,7 +1884,7 @@ static std::string build_system_prompt(const std::vector &knowledge "## Tool protocol\n" "Emit tool calls on their own line. The host executes them and returns\n" "TOOL_RESULT: on the next line.\n\n" - "## Available tools:\n" + "Available tools:\n" " TOOL:LIST [dir] list files (default: sandbox root)\n" " TOOL:READ read file contents\n" " TOOL:WRITE write text to file\n" @@ -1828,25 +1895,12 @@ static std::string build_system_prompt(const std::vector &knowledge " TOOL:RND random float\n" " TOOL:PERMISSION ask user for explicit permission\n" " TOOL:CURL HTTP GET; returns response body (max 32 KB)\n\n" - "## Rules:\n" + "Rules:\n" "- Never access files outside the sandbox.\n" - "- Use TOOL:PERMISSION when you're about to modify files, delete data, or run external programs.\n" + "- Use TOOL:PERMISSION before destructive or irreversible operations.\n" "- Use TOOL:CURL to fetch documentation, APIs, or web content you need.\n" "- Reason step-by-step inside <|think|>… (hidden from user).\n" - "- After each tool call, explain what you did in plain English.\n" - "Ask the user for explicit permission before proceeding.\n\n" - "## File Reading\n" - "When you read a file with TOOL:READ, you MUST:\n" - "1. Acknowledge what you found\n" - "2. Use that information in your response\n" - "3. If the file contains code, explain it or show relevant parts\n" - "4. If the file contains documentation, summarize key points\n" - "## Tool Result Integration\n" - "When you see TOOL_RESULT in the conversation, it contains tool output.\n" - "Use it to:\n" - "- Answer questions based on file contents\n" - "- Explain code or configuration\n" - "- Provide accurate information from the file\n"; + "- After each tool call, explain what you did in plain English.\n\n"; for (const auto &kf : knowledge_files) { std::ifstream f(kf); if (!f) continue; @@ -1872,21 +1926,7 @@ static void handle_slash(const std::string &input, } if (verb == "/help") { - tui.append_line("[sys] Commands:"); - tui.append_line("[sys] /model [path] load a GGUF model (picker if no path)"); - tui.append_line("[sys] /embed [path] load an embedding model (picker if no path)"); - tui.append_line("[sys] /rag [path] index file or directory (picker if no path)"); - tui.append_line("[sys] /memory KV / VRAM / layer stats"); - tui.append_line("[sys] /clear reset conversation"); - tui.append_line("[sys] /settings show current settings"); - tui.append_line("[sys] /set change a setting live"); - tui.append_line("[sys] /help this message"); - tui.append_line("[sys] exit / quit exit Nitro"); - tui.append_line("[sys] Settable keys (via /set):"); - tui.append_line("[sys] temperature top_p top_k min_p penalty_repeat"); - tui.append_line("[sys] n_max_tokens penalty_last_n rag_top_k n_gpu_layers"); - tui.append_line("[sys] run_allowed (comma-separated list, e.g. python3,make)"); - tui.redraw_all(); + tui.show_help(); return; } // ── /model ────────────────────────────────────────────────────────────── @@ -2111,8 +2151,7 @@ int main(int argc, char **argv) { } else if (a == "-g" || a == "--gpu-layers") { cfg.n_gpu_layers = std::stoi(take_next(a.c_str())); } else if (a == "-h" || a == "--help") { - std::puts( - "Usage: nitro [options] [project_dir]\n" + std::puts("Usage: nitro [options] [project_dir]\n" "\n" "Options:\n" " -m, --model GGUF model to load on startup\n" From 17a6e5fa728bc36baa55acdab22052c014e2b351 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Sun, 24 May 2026 09:15:22 +0930 Subject: [PATCH 115/131] LLAMN: added nitro introspection tool --- llama/nitro.cpp | 88 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 58 insertions(+), 30 deletions(-) diff --git a/llama/nitro.cpp b/llama/nitro.cpp index ece71cb..2fed672 100644 --- a/llama/nitro.cpp +++ b/llama/nitro.cpp @@ -70,11 +70,8 @@ static bool write_file(const std::string &path, const std::string &data) static std::string list_dir(const std::string &path); static bool path_in_sandbox(const std::string &sandbox, const std::string &path); static std::string strip_code_fences(const std::string &filename, const std::string &src); -static std::string process_tool(const std::string &line, const std::string &sandbox, - const std::vector &run_allowed, - TuiState &tui); -static std::string build_system_prompt(const std::vector &knowledge_files, - const std::string &sandbox); +static std::string process_tool(const std::string &line, const NitroConfig &cfg, TuiState &tui); +static std::string build_system_prompt(const std::vector &knowledge_files, const std::string &sandbox); // ─── RAG indexing ───────────────────────────────────────────────────────────── static constexpr int BATCH_SIZE = 512; @@ -388,6 +385,41 @@ static std::string json_escape(const std::string &s) { return out; } +static std::string introspect(const NitroConfig &cfg) { + static constexpr std::string_view tmpl = + "{{\n" + " \"model_path\": \"{}\",\n" + " \"embed_path\": \"{}\",\n" + " \"sandbox\": \"{}\",\n" + " \"n_ctx\": {},\n" + " \"n_batch\": {},\n" + " \"n_gpu_layers\": {},\n" + " \"n_max_tokens\": {},\n" + " \"temperature\": {},\n" + " \"top_p\": {},\n" + " \"min_p\": {},\n" + " \"top_k\": {},\n" + " \"penalty_repeat\": {},\n" + " \"penalty_last_n\": {},\n" + " \"rag_top_k\": {}\n" + "}}\n"; + return std::format(tmpl, + cfg.model_path, + cfg.embed_path, + cfg.sandbox, + cfg.n_ctx, + cfg.n_batch, + cfg.n_gpu_layers, + cfg.n_max_tokens, + cfg.temperature, + cfg.top_p, + cfg.min_p, + cfg.top_k, + cfg.penalty_repeat, + cfg.penalty_last_n, + cfg.rag_top_k); +} + // Persist the current cfg to ~/.config/nitro.settings.json. static bool save_settings(const NitroConfig &cfg) { std::string path = settings_path(); @@ -397,24 +429,11 @@ static bool save_settings(const NitroConfig &cfg) { fs::create_directories(dir, ec); std::ofstream f(path, std::ios::trunc); - if (!f) return false; + if (!f) { + return false; + } - f << "{\n"; - f << " \"model_path\": \"" << json_escape(cfg.model_path) << "\",\n"; - f << " \"embed_path\": \"" << json_escape(cfg.embed_path) << "\",\n"; - f << " \"sandbox\": \"" << json_escape(cfg.sandbox) << "\",\n"; - f << " \"n_ctx\": " << cfg.n_ctx << ",\n"; - f << " \"n_batch\": " << cfg.n_batch << ",\n"; - f << " \"n_gpu_layers\": " << cfg.n_gpu_layers << ",\n"; - f << " \"n_max_tokens\": " << cfg.n_max_tokens << ",\n"; - f << " \"temperature\": " << cfg.temperature << ",\n"; - f << " \"top_p\": " << cfg.top_p << ",\n"; - f << " \"min_p\": " << cfg.min_p << ",\n"; - f << " \"top_k\": " << cfg.top_k << ",\n"; - f << " \"penalty_repeat\": " << cfg.penalty_repeat << ",\n"; - f << " \"penalty_last_n\": " << cfg.penalty_last_n << ",\n"; - f << " \"rag_top_k\": " << cfg.rag_top_k << "\n"; - f << "}\n"; + f << introspect(cfg); return f.good(); } @@ -1195,6 +1214,7 @@ struct AgentState { std::string memory_info_text(); float tokens_per_sec() const; }; + void AgentState::apply_generation_params(const NitroConfig &cfg) { llama->add_stop("<|turn|>"); llama->add_stop("<|im_end|>"); @@ -1461,7 +1481,7 @@ bool AgentState::run_turn(const std::string &user_message, tui.append_line("[tool] " + op + " " + arg1 + (op == "TOOL:WRITE" ? " " : "")); tui.redraw_all(); - std::string result = process_tool(tool_line, cfg.sandbox, cfg.run_allowed, tui); + std::string result = process_tool(tool_line, cfg, tui); tui.append_line("[tool] β†’ " + result.substr(0, 200) + (result.size() > 200 ? "…" : "")); tui.redraw_all(); @@ -1486,7 +1506,7 @@ bool AgentState::run_turn(const std::string &user_message, std::string trimmed = buffer; trimmed.erase(0, trimmed.find_first_not_of(" \t")); if (trimmed.substr(0, 5) == "TOOL:") { - std::string result = process_tool(trimmed, cfg.sandbox, cfg.run_allowed, tui); + std::string result = process_tool(trimmed, cfg, tui); tui.append_line("[tool] β†’ " + result.substr(0, 200)); tui.redraw_all(); std::string tool_result_msg = "TOOL_RESULT: " + result; @@ -1777,10 +1797,10 @@ static std::string tool_curl(const std::string &url) { // ═══════════════════════════════════════════════════════════════════════════ // Tool dispatch // ═══════════════════════════════════════════════════════════════════════════ -static std::string process_tool(const std::string &cmd, - const std::string &sandbox, - const std::vector &run_allowed, - TuiState &tui) { +static std::string process_tool(const std::string &cmd, const NitroConfig &cfg, TuiState &tui) { + const std::string &sandbox = cfg.sandbox; + const std::vector &run_allowed = cfg.run_allowed; + std::string op, arg1, arg2; auto sp1 = cmd.find(' '); if (sp1 == std::string::npos) { @@ -1848,6 +1868,9 @@ static std::string process_tool(const std::string &cmd, if (op == "TOOL:CURL") { return tool_curl(arg1); } + if (op == "TOOL:INTROSPECT") { + return introspect(cfg); + } if (op == "TOOL:RUN") { std::string prog = resolve(arg1); if (!path_in_sandbox(sandbox, prog)) return "ERROR: path outside sandbox"; @@ -1894,6 +1917,7 @@ static std::string build_system_prompt(const std::vector &knowledge " TOOL:TIME current time\n" " TOOL:RND random float\n" " TOOL:PERMISSION ask user for explicit permission\n" + " TOOL:INTROSPECT introspect your settings, top_k etc\n" " TOOL:CURL HTTP GET; returns response body (max 32 KB)\n\n" "Rules:\n" "- Never access files outside the sandbox.\n" @@ -2084,8 +2108,9 @@ static void handle_slash(const std::string &input, } if (ok) { - if (needs_reparam && agent.model_loaded) + if (needs_reparam && agent.model_loaded) { agent.apply_generation_params(cfg); + } save_settings(cfg); tui.append_line("[sys] " + key + " = " + val); } @@ -2185,7 +2210,10 @@ int main(int argc, char **argv) { std::error_code ec; cfg.sandbox = fs::current_path(ec).string(); } - { std::error_code ec; fs::create_directories(cfg.sandbox, ec); } + { + std::error_code ec; + fs::create_directories(cfg.sandbox, ec); + } // ── Auto-discover knowledge files ───────────────────────────────── for (const char *kf : {"nitro.md", "AGENTS.md", "README.md"}) { From 4737d150624dcf18c21877a829da078a0c3e28e5 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Mon, 25 May 2026 20:02:02 +0930 Subject: [PATCH 116/131] LLAMA: fix nitro tool handling --- llama/llama-sb.cpp | 54 ++++--- llama/llama-sb.h | 26 ++-- llama/nitro.cpp | 367 +++++++++++++++++++++++++-------------------- 3 files changed, 250 insertions(+), 197 deletions(-) diff --git a/llama/llama-sb.cpp b/llama/llama-sb.cpp index 6a74c69..a4ad885 100644 --- a/llama/llama-sb.cpp +++ b/llama/llama-sb.cpp @@ -61,8 +61,9 @@ Llama::Llama() : _max_tokens(0), _log_level(GGML_LOG_LEVEL_CONT), _n_gpu_layers(0), - _n_past(0), + _n_system_tokens(0), _is_gemma4(false), + _sampler_dirty(false), _seed(LLAMA_DEFAULT_SEED) { llama_log_set([](enum ggml_log_level level, const char *text, void *user_data) { Llama *llama = (Llama *)user_data; @@ -99,8 +100,9 @@ Llama::Llama(Llama &&other) noexcept , _max_tokens(other._max_tokens) , _log_level(other._log_level) , _n_gpu_layers(other._n_gpu_layers) - , _n_past(other._n_past) + , _n_system_tokens(other._n_system_tokens) , _is_gemma4(other._is_gemma4) + , _sampler_dirty(other._sampler_dirty) , _seed(other._seed) { } @@ -129,8 +131,9 @@ void Llama::reset() { _top_p = 1.0f; _min_p = 0.0f; _max_tokens = 150; - _n_past = 0; + _n_system_tokens = 0; _seed = LLAMA_DEFAULT_SEED; + _sampler_dirty = true; if (_ctx) { llama_memory_clear(llama_get_memory(_ctx), true); } @@ -210,13 +213,13 @@ bool Llama::load_embedding_model(string model_path) { void Llama::set_grammar(const string &src, const string &root) { _grammar_src = src; _grammar_root = root; + dirty(); } bool Llama::add_message(LlamaIter &iter, const string &role, const string &content) { llama_chat_message message = {role.c_str(), content.c_str()}; int buf_size = 2 * (int)(role.size() + content.size() + 64); vector buf(buf_size); - bool add_ass = (role == "user" || role == "tool"); int32_t n = 0; if (_template.empty()) { @@ -225,14 +228,18 @@ bool Llama::add_message(LlamaIter &iter, const string &role, const string &conte } if (_is_gemma4) { - string str = "<|turn>" + role + "\n" + content + "\n"; - if (add_ass) { - str += "<|turn>model\n"; + // see: https://ai.google.dev/gemma/docs/core/prompt-formatting-gemma4 + string str; + if (role == "system") { + str = "<|turn>system\n<|think|>" + content + "\n"; + } else { + str = "<|turn>" + role + "\n" + content + "\n"; } n = str.size(); buf.assign(str.begin(), str.end()); buf.push_back('\0'); } else { + bool add_ass = (role == "user" || role == "tool" || role == "tool_result"); n = llama_chat_apply_template(_template.c_str(), &message, 1, add_ass, buf.data(), buf_size); if (n < 0) { _last_error = "No chat template no supported"; @@ -244,8 +251,12 @@ bool Llama::add_message(LlamaIter &iter, const string &role, const string &conte } string prompt(buf.data(), n); - if (!configure_sampler()) { - return false; + if (_sampler_dirty) { + // avoid wasteful rebuild + if (!configure_sampler()) { + return false; + } + _sampler_dirty = false; } vector prompt_tokens = tokenize(prompt); @@ -253,7 +264,12 @@ bool Llama::add_message(LlamaIter &iter, const string &role, const string &conte return false; } - if (!make_space_for_tokens(prompt_tokens.size(), _n_past)) { + if (role == "system") { + // always retain system tokens + _n_system_tokens = prompt_tokens.size(); + } + + if (!make_space_for_tokens(prompt_tokens.size())) { return false; } @@ -278,7 +294,6 @@ bool Llama::add_message(LlamaIter &iter, const string &role, const string &conte } } - _n_past += prompt_tokens.size(); iter._t_start = std::chrono::high_resolution_clock::now(); iter._llama = this; iter._has_next = true; @@ -326,7 +341,6 @@ string Llama::all(LlamaIter &iter) { // end-of-generation check if (llama_vocab_is_eog(_vocab, tok)) { - iter._has_next = false; break; } @@ -342,6 +356,9 @@ string Llama::all(LlamaIter &iter) { } } + // tokens exhausted - call add_message to continue + iter._has_next = false; + // detokenize sequentially if (!decoded.empty()) { for (llama_token tok : decoded) { @@ -407,8 +424,8 @@ bool Llama::batch_decode_tokens(vector &tokens) { llama_batch batch = llama_batch_get_one(tokens.data() + i, batch_size); int result = llama_decode(_ctx, batch); if (result != 0) { - _last_error = std::format("Failed to decode batch. position:{} error:{} [size:{}, past:{}]", - i, result, tokens.size(), _n_past); + _last_error = std::format("Failed to decode batch. position:{} error:{} [size:{}]", + i, result, tokens.size()); return false; } } @@ -507,9 +524,8 @@ bool Llama::ends_with_sentence_boundary(const string &text) { // // Parameters: // n_tokens - Number of tokens we need space for -// keep_min - Minimum tokens to keep (e.g., system prompt), default 0 // -bool Llama::make_space_for_tokens(int n_tokens, int keep_min) { +bool Llama::make_space_for_tokens(int n_tokens) { int n_ctx = llama_n_ctx(_ctx); if (n_tokens > n_ctx) { _last_error = "Too many tokens, increase context size (n_ctx)"; @@ -539,10 +555,10 @@ bool Llama::make_space_for_tokens(int n_tokens, int keep_min) { // Calculate how many tokens to remove int tokens_to_remove = space_needed - space_available; - // Can't remove more than we have (minus keep_min) - int removable = current_used - keep_min; + // Can't remove more than we have (minus _n_system_tokens) + int removable = current_used - _n_system_tokens; if (tokens_to_remove > removable) { - _last_error = "Can't make enough space while keeping keep_min tokens"; + _last_error = "Can't make enough space while keeping num_system_tokens tokens"; return false; } diff --git a/llama/llama-sb.h b/llama/llama-sb.h index 0ca3998..43b7c7c 100644 --- a/llama/llama-sb.h +++ b/llama/llama-sb.h @@ -81,17 +81,17 @@ struct Llama { // generation parameters void add_stop(const char *stop) { _stop_sequences.push_back(stop); } void clear_stops() { _stop_sequences.clear(); } - void set_penalty_last_n(int32_t penalty_last_n) { _penalty_last_n = penalty_last_n; } - void set_penalty_repeat(float penalty_repeat) { _penalty_repeat = penalty_repeat; } - void set_penalty_freq(float penalty_freq) { _penalty_freq = penalty_freq; } - void set_penalty_present(float penalty_present) { _penalty_present = penalty_present; } - void set_max_tokens(int max_tokens) { _max_tokens = max_tokens; } - void set_min_p(float min_p) { _min_p = min_p; } - void set_temperature(float temperature) { _temperature = temperature; } - void set_top_k(int top_k) { _top_k = top_k; } - void set_top_p(float top_p) { _top_p = top_p; } + void set_penalty_last_n(int32_t penalty_last_n) { _penalty_last_n = penalty_last_n; dirty(); } + void set_penalty_repeat(float penalty_repeat) { _penalty_repeat = penalty_repeat; dirty(); } + void set_penalty_freq(float penalty_freq) { _penalty_freq = penalty_freq; dirty(); } + void set_penalty_present(float penalty_present) { _penalty_present = penalty_present; dirty(); } + void set_max_tokens(int max_tokens) { _max_tokens = max_tokens; dirty(); } + void set_min_p(float min_p) { _min_p = min_p; dirty(); } + void set_temperature(float temperature) { _temperature = temperature; dirty(); } + void set_top_k(int top_k) { _top_k = top_k; dirty(); } + void set_top_p(float top_p) { _top_p = top_p; dirty(); } void set_grammar(const string &src, const string &root); - void set_seed(unsigned int seed) { _seed = seed; } + void set_seed(unsigned int seed) { _seed = seed; dirty(); } // error handling const char *last_error() { return _last_error.c_str(); } @@ -110,8 +110,9 @@ struct Llama { private: bool batch_decode_tokens(vector &tokens); bool configure_sampler(); + void dirty() {_sampler_dirty = true; } bool ends_with_sentence_boundary(const string &out); - bool make_space_for_tokens(int n_tokens, int keep_min); + bool make_space_for_tokens(int n_tokens); vector tokenize(const string &prompt); string token_to_string(LlamaIter &iter, llama_token tok); void set_last_error(const char *message); @@ -136,7 +137,8 @@ struct Llama { int _max_tokens; int _log_level; int _n_gpu_layers; - int _n_past; + int _n_system_tokens; bool _is_gemma4; + bool _sampler_dirty; unsigned int _seed; }; diff --git a/llama/nitro.cpp b/llama/nitro.cpp index 2fed672..bdff9ea 100644 --- a/llama/nitro.cpp +++ b/llama/nitro.cpp @@ -252,6 +252,38 @@ class InputHistory { int current_index = 0; }; +// ═══════════════════════════════════════════════════════════════════════════ +// Logging +// ═══════════════════════════════════════════════════════════════════════════ +// ─── Debug logging (file-backed, safe to call while notcurses is active) ── +static FILE *g_logfile = nullptr; + +static void log_open() { + const char *home = getenv("HOME"); + std::string path = std::string(home ? home : ".") + "/.config/nitro.log"; + g_logfile = fopen(path.c_str(), "a"); +} + +static void log_close() { + if (g_logfile) { fclose(g_logfile); g_logfile = nullptr; } +} + +static void log_write(const char *fmt, ...) __attribute__((format(printf, 1, 2))); +static void log_write(const char *fmt, ...) { + if (!g_logfile) return; + // timestamp + time_t t = time(nullptr); + char ts[32]; + strftime(ts, sizeof(ts), "%H:%M:%S", localtime(&t)); + fprintf(g_logfile, "[%s] ", ts); + va_list ap; + va_start(ap, fmt); + vfprintf(g_logfile, fmt, ap); + va_end(ap); + fputc('\n', g_logfile); + fflush(g_logfile); // flush immediately so tail -f works +} + // ═══════════════════════════════════════════════════════════════════════════ // Settings persistence (~/.config/nitro.settings.json) // ═══════════════════════════════════════════════════════════════════════════ @@ -326,14 +358,26 @@ static bool settings_get_float(const std::string &json, float &out) { std::string search = "\"" + key + "\":"; size_t pos = json.find(search); - if (pos == std::string::npos) return false; + if (pos == std::string::npos) { + return false; + } pos += search.size(); - while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t')) ++pos; - if (pos >= json.size()) return false; + while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t')) { + ++pos; + } + if (pos >= json.size()) { + return false; + } size_t start = pos; - if (json[pos] == '-') ++pos; - while (pos < json.size() && (std::isdigit((unsigned char)json[pos]) || json[pos] == '.')) ++pos; - if (pos == start) return false; + if (json[pos] == '-') { + ++pos; + } + while (pos < json.size() && (std::isdigit((unsigned char)json[pos]) || json[pos] == '.')) { + ++pos; + } + if (pos == start) { + return false; + } out = std::stof(json.substr(start, pos - start)); return true; } @@ -438,6 +482,23 @@ static bool save_settings(const NitroConfig &cfg) { return f.good(); } +// Trims whitespace from both ends of a string +std::string trim(std::string_view str) { + const std::string_view whitespace = " \t\n\r\f\v"; + + // Find the first non-whitespace character + const auto start = str.find_first_not_of(whitespace); + if (start == std::string_view::npos) { + return ""; // The string is entirely whitespace + } + + // Find the last non-whitespace character + const auto end = str.find_last_not_of(whitespace); + + // Return the substring between start and end + return std::string(str.substr(start, end - start + 1)); +} + // ═══════════════════════════════════════════════════════════════════════════ // Notcurses TUI // ═══════════════════════════════════════════════════════════════════════════ @@ -580,9 +641,11 @@ void TuiState::init() { notcurses_mice_enable(nc, NCMICE_BUTTON_EVENT); redraw_all(); } + void TuiState::destroy() { if (nc) { notcurses_stop(nc); nc = nullptr; } } + void TuiState::resize() { notcurses_term_dim_yx(nc, (unsigned *)&term_rows, (unsigned *)&term_cols); ncplane_resize_simple(header, 1, (unsigned)term_cols); @@ -678,13 +741,13 @@ void TuiState::redraw_input() { ncplane_set_channels(inputpl, inp_ch(230, 230, 230)); ncplane_putstr_yx(inputpl, 1, prompt_cols, before.c_str()); int cx = prompt_cols + cur_in_view; - ncplane_set_channels(inputpl, - NCCHANNELS_INITIALIZER(BG_INP_R, BG_INP_G, BG_INP_B, 180, 230, 255)); + ncplane_set_channels(inputpl, NCCHANNELS_INITIALIZER(BG_INP_R, BG_INP_G, BG_INP_B, 180, 230, 255)); char cbuf[2] = { cursor_ch_val, '\0' }; ncplane_putstr_yx(inputpl, 1, cx, cbuf); ncplane_set_channels(inputpl, inp_ch(230, 230, 230)); - if (!after.empty()) + if (!after.empty()) { ncplane_putstr_yx(inputpl, 1, cx + 1, after.c_str()); + } } void TuiState::redraw_all() { @@ -714,8 +777,9 @@ void TuiState::append_line(const std::string &line) { if ((int)line.size() <= w) { chat_lines.push_back(line); } else { - for (int off = 0; off < (int)line.size(); off += w) + for (int off = 0; off < (int)line.size(); off += w) { chat_lines.push_back(line.substr(off, w)); + } } } @@ -723,7 +787,9 @@ void TuiState::append_token(const std::string &token) { token_acc += token; for (;;) { auto pos = token_acc.find('\n'); - if (pos == std::string::npos) break; + if (pos == std::string::npos) { + break; + } append_line(token_acc.substr(0, pos)); token_acc = token_acc.substr(pos + 1); } @@ -895,8 +961,7 @@ std::string TuiState::file_picker(const std::string &start_dir, if (!picker) return ""; static constexpr uint32_t PBG_R = 18, PBG_G = 24, PBG_B = 40; - ncplane_set_base(picker, " ", 0, - NCCHANNELS_INITIALIZER(PBG_R, PBG_G, PBG_B, PBG_R, PBG_G, PBG_B)); + ncplane_set_base(picker, " ", 0, NCCHANNELS_INITIALIZER(PBG_R, PBG_G, PBG_B, PBG_R, PBG_G, PBG_B)); // Build a compact hint line appropriate to the operation. // /rag adds 's=select dir'; /model and /embed only need file selection. std::string hint_line = "↑↓ navigate Enter open/select Esc cancel"; @@ -920,8 +985,7 @@ std::string TuiState::file_picker(const std::string &start_dir, ncplane_putstr_yx(picker, PH - 1, PW - 1, "╝"); // Title - ncplane_set_channels(picker, - NCCHANNELS_INITIALIZER(255, 220, 80, PBG_R, PBG_G, PBG_B)); + ncplane_set_channels(picker, NCCHANNELS_INITIALIZER(255, 220, 80, PBG_R, PBG_G, PBG_B)); std::string title_str = " πŸ“‚ " + title_hint + " Picker "; if ((int)title_str.size() > PW - 4) title_str = title_str.substr(0, PW - 4); ncplane_putstr_yx(picker, 0, 2, title_str.c_str()); @@ -929,12 +993,10 @@ std::string TuiState::file_picker(const std::string &start_dir, std::string path_display = current_dir; if ((int)path_display.size() > PW - 4) path_display = "…" + path_display.substr(path_display.size() - (PW - 5)); - ncplane_set_channels(picker, - NCCHANNELS_INITIALIZER(160, 200, 240, PBG_R, PBG_G, PBG_B)); + ncplane_set_channels(picker, NCCHANNELS_INITIALIZER(160, 200, 240, PBG_R, PBG_G, PBG_B)); ncplane_putstr_yx(picker, 1, 2, path_display.c_str()); // Hint line (bottom interior row). - ncplane_set_channels(picker, - NCCHANNELS_INITIALIZER(120, 120, 160, PBG_R, PBG_G, PBG_B)); + ncplane_set_channels(picker, NCCHANNELS_INITIALIZER(120, 120, 160, PBG_R, PBG_G, PBG_B)); std::string hint_trunc = hint_line; if ((int)hint_trunc.size() > PW - 4) hint_trunc = hint_trunc.substr(0, PW - 4); ncplane_putstr_yx(picker, PH - 2, 2, hint_trunc.c_str()); @@ -954,8 +1016,7 @@ std::string TuiState::file_picker(const std::string &start_dir, uint32_t br = is_selected ? 100 : PBG_R; uint32_t bg = is_selected ? 180 : PBG_G; uint32_t bb = is_selected ? 255 : PBG_B; - ncplane_set_channels(picker, - NCCHANNELS_INITIALIZER(fr, fg, fb, br, bg, bb)); + ncplane_set_channels(picker, NCCHANNELS_INITIALIZER(fr, fg, fb, br, bg, bb)); std::string label = (is_selected ? " β–Ά " : " ") + entries[idx]; if ((int)label.size() > PW - 2) label = label.substr(0, PW - 2); while ((int)label.size() < PW - 2) label += ' '; @@ -1216,6 +1277,7 @@ struct AgentState { }; void AgentState::apply_generation_params(const NitroConfig &cfg) { + // llama->add_stop(MARKER_END_TOOL); llama->add_stop("<|turn|>"); llama->add_stop("<|im_end|>"); llama->set_max_tokens(cfg.n_max_tokens); @@ -1347,40 +1409,6 @@ bool AgentState::rag_index(const std::string &path, TuiState &tui) { return true; } -// ═══════════════════════════════════════════════════════════════════════════ -// Think-tag filtering -// ═══════════════════════════════════════════════════════════════════════════ -// Strips everything between (and including) … or the -// <|think|>… variant from a completed line/buffer. -// Also strips any bare close-tag that appears without a matching open-tag -// (can happen when the open was in a previous chunk already consumed). -// Returns the visible text that should be shown to the user. -std::string filter_think_tags(const std::string &text) { - static const struct { const char *open; const char *close; } PAIRS[] = { - { "", "" }, - { "<|think|>", "" }, - }; - std::string out = text; - for (auto &p : PAIRS) { - std::string open(p.open), close(p.close); - for (;;) { - auto ob = out.find(open); - if (ob == std::string::npos) break; - auto ce = out.find(close, ob); - if (ce == std::string::npos) { - out.erase(ob); // no closing tag β€” strip to end - break; - } - out.erase(ob, ce + close.size() - ob); - } - // Strip orphan close-tags (open tag was in an earlier chunk). - size_t pos = 0; - while ((pos = out.find(close, pos)) != std::string::npos) - out.erase(pos, close.size()); - } - return out; -} - // ═══════════════════════════════════════════════════════════════════════════ // Agent turn // ═══════════════════════════════════════════════════════════════════════════ @@ -1411,110 +1439,88 @@ bool AgentState::run_turn(const std::string &user_message, return false; } tui.append_line("Nitro: "); + // in_think starts false β€” models that don't use blocks emit // visible text immediately. The spinner activates only while thinking. - bool in_think = false; + enum {t_init, t_think, t_thunk} think_mode = t_init; tui.set_thinking(false); std::string buffer; + + auto invoke_tool = [&](const std::string &buffer, const std::string_view template_str) -> void { + std::string result = process_tool(buffer, cfg, tui); + std::string content = std::vformat(template_str, std::make_format_args(result)); + if (!llama->add_message(*iter, "tool_result", content)) { + tui.append_line(std::string("[err] tool result inject: ") + llama->last_error()); + tui.redraw_all(); + } + if (!iter->_has_next) { + tui.append_line(std::string("[err] failed to evoke tool response: ") + llama->last_error()); + tui.redraw_all(); + } + }; + + auto start_think = [&](const std::string &tag) { + if (think_mode == t_init) { + auto pos = buffer.find(tag); + if (pos != std::string::npos) { + think_mode = t_think; + tui.set_thinking(true); + // display prededing text + buffer = buffer.substr(0, pos); + } + } + }; + + auto end_think = [&](const std::string &tag) { + if (think_mode == t_think) { + auto pos = buffer.find(tag); + if (pos != std::string::npos) { + think_mode = t_thunk; + tui.set_thinking(false); + // display remaining text + buffer = buffer.substr(pos + tag.length()); + } + } + }; + while (iter->_has_next) { std::string tok = llama->next(*iter); buffer += tok; - // Detect think-tag transitions on the accumulated buffer so we never - // miss a tag that was split across two tokens. - bool was_thinking = in_think; - if (!in_think) { - if (buffer.find("") != std::string::npos || - buffer.find("<|think|>") != std::string::npos) - in_think = true; - } - if (in_think) { - if (buffer.find("") != std::string::npos || - buffer.find("") != std::string::npos) - in_think = false; - } - // Update spinner: animate only while in a think block. - if (in_think || was_thinking) { - tui.set_thinking(in_think); - if (in_think) tui.tick_spinner(); - } - auto nl = buffer.find('\n'); - if (nl != std::string::npos) { - std::string text_line = buffer.substr(0, nl); - buffer = buffer.substr(nl + 1); - std::string trimmed = text_line; - trimmed.erase(0, trimmed.find_first_not_of(" \t")); - if (trimmed.substr(0, 5) == "TOOL:") { - std::string tail = buffer + llama->all(*iter); - std::string op, arg1, payload; - { - auto s1 = trimmed.find(' '); - if (s1 != std::string::npos) { - op = trimmed.substr(0, s1); - std::string rest = trimmed.substr(s1 + 1); - rest.erase(0, rest.find_first_not_of(" \t")); - auto s2 = rest.find(' '); - if (s2 != std::string::npos) { - arg1 = rest.substr(0, s2); - payload = rest.substr(s2 + 1) + tail; - } else { - arg1 = rest; - payload = tail; - } - } else { - op = trimmed; - } - } - std::string tool_line; - if (op == "TOOL:WRITE") { - tool_line = op + " " + arg1 + " " + payload; - while (!tool_line.empty() && tool_line.back() == '\n') tool_line.pop_back(); - } else { - tool_line = op; - if (!arg1.empty()) tool_line += " " + arg1; - if (!payload.empty()) { - std::string flat = payload; - flat.erase(std::remove(flat.begin(), flat.end(), '\n'), flat.end()); - while (!flat.empty() && std::isspace((unsigned char)flat.back())) flat.pop_back(); - if (!flat.empty()) tool_line += " " + flat; - } - } - tui.append_line("[tool] " + op + " " + arg1 + - (op == "TOOL:WRITE" ? " " : "")); - tui.redraw_all(); - std::string result = process_tool(tool_line, cfg, tui); - tui.append_line("[tool] β†’ " + - result.substr(0, 200) + (result.size() > 200 ? "…" : "")); - tui.redraw_all(); - // Inject the tool result back into the conversation as a user-role - // message using the TOOL_RESULT: prefix that the system prompt - // describes. Using "user" role ensures the injected text is correctly - // formatted regardless of whether the underlying llama-sb layer knows - // a dedicated "tool" role. - std::string tool_result_msg = "TOOL_RESULT: " + result; - if (!llama->add_message(*iter, "user", tool_result_msg)) { - tui.append_line(std::string("[err] tool result inject: ") + llama->last_error()); - tui.redraw_all(); - break; - } + + if (think_mode == t_init) { + start_think(""); + start_think("<|think|>"); + start_think(""); + start_think("<|channel>thought"); + } + if (think_mode == t_think) { + tui.tick_spinner(); + end_think(""); + end_think(""); + end_think(""); + end_think(""); + } + if (think_mode == t_thunk) { + auto tool_start = buffer.find("TOOL:"); + if (tool_start != std::string::npos) { + // fetch all remaining tokens + invoke_tool(buffer + llama->all(*iter), "TOOL_RESULT: {}"); buffer.clear(); - } else if (!in_think) { - tui.append_token(filter_think_tags(text_line) + "\n"); + think_mode = t_init; + continue; + } + auto pos = buffer.find('\n'); + if (pos != std::string::npos && pos > 0) { + tui.append_token(buffer.substr(0, pos) + "\n"); + buffer = buffer.substr(pos + 1); } } } + if (!buffer.empty()) { - std::string trimmed = buffer; - trimmed.erase(0, trimmed.find_first_not_of(" \t")); - if (trimmed.substr(0, 5) == "TOOL:") { - std::string result = process_tool(trimmed, cfg, tui); - tui.append_line("[tool] β†’ " + result.substr(0, 200)); - tui.redraw_all(); - std::string tool_result_msg = "TOOL_RESULT: " + result; - llama->add_message(*iter, "user", tool_result_msg); - } else if (!in_think) { - tui.append_token(filter_think_tags(buffer)); - } + tui.append_token(buffer + "\n"); } + tui.flush_token_acc(); tui.set_thinking(false); tui.tokens_per_sec = tokens_per_sec(); @@ -1556,7 +1562,9 @@ static bool path_in_sandbox(const std::string &sandbox, const std::string &path) static std::string read_file(const std::string &path) { std::ifstream f(path, std::ios::binary); - if (!f) return "ERROR: cannot open " + path; + if (!f) { + return "ERROR: cannot open [" + path + "]"; + } std::ostringstream oss; oss << f.rdbuf(); return oss.str(); } @@ -1661,8 +1669,11 @@ static std::string html_to_text(const std::string &html) { size_t sp = inner.find_first_of(" \t/\r\n"); std::string name = (sp != std::string::npos) ? inner.substr(0, sp) : inner; std::transform(name.begin(), name.end(), name.begin(), ::tolower); - for (int k = 0; BLOCK[k]; ++k) - if (name == BLOCK[k]) { out += '\n'; break; } + for (int k = 0; BLOCK[k]; ++k) { + if (name == BLOCK[k]) { + out += '\n'; break; + } + } i = ce + 1; } s = out; @@ -1750,6 +1761,7 @@ static size_t curl_write_cb(void *contents, size_t size, size_t nmemb, void *use } return total; } + static std::string tool_curl(const std::string &url) { if (url.empty()) return "ERROR: TOOL:CURL requires a URL argument"; CURL *curl = curl_easy_init(); @@ -1804,9 +1816,9 @@ static std::string process_tool(const std::string &cmd, const NitroConfig &cfg, std::string op, arg1, arg2; auto sp1 = cmd.find(' '); if (sp1 == std::string::npos) { - op = cmd; + op = trim(cmd); } else { - op = cmd.substr(0, sp1); + op = trim(cmd.substr(0, sp1)); std::string rest = cmd.substr(sp1 + 1); rest.erase(0, rest.find_first_not_of(" \t")); auto sp2 = rest.find(' '); @@ -1825,6 +1837,9 @@ static std::string process_tool(const std::string &cmd, const NitroConfig &cfg, return join_path(sandbox, p); }; + tui.append_line("[tool] β†’ " + op); + tui.redraw_all(); + if (op == "TOOL:DATE") { char buf[32]; time_t t = time(nullptr); strftime(buf, sizeof(buf), "%Y-%m-%d", localtime(&t)); @@ -1873,7 +1888,9 @@ static std::string process_tool(const std::string &cmd, const NitroConfig &cfg, } if (op == "TOOL:RUN") { std::string prog = resolve(arg1); - if (!path_in_sandbox(sandbox, prog)) return "ERROR: path outside sandbox"; + if (!path_in_sandbox(sandbox, prog)) { + return "ERROR: path outside sandbox"; + } if (!run_allowed.empty()) { std::string basename = fs::path(prog).filename().string(); bool permitted = std::any_of(run_allowed.begin(), run_allowed.end(), @@ -1885,15 +1902,21 @@ static std::string process_tool(const std::string &cmd, const NitroConfig &cfg, } std::string command = prog + " " + arg2 + " 2>&1"; FILE *fp = popen(command.c_str(), "r"); - if (!fp) return "ERROR: popen failed"; + if (!fp) { + return "ERROR: popen failed"; + } std::string out; char buf[256]; - while (fgets(buf, sizeof(buf), fp)) out += buf; + while (fgets(buf, sizeof(buf), fp)) { + out += buf; + } pclose(fp); - if (out.size() > 4096) out = out.substr(0, 4096) + "\n…(truncated)"; + if (out.size() > 4096) { + out = out.substr(0, 4096) + "\n…(truncated)"; + } return out; } - return "ERROR: unknown tool: " + op; + return "ERROR: unknown tool: [" + op + "]"; } // ═══════════════════════════════════════════════════════════════════════════ @@ -1905,8 +1928,9 @@ static std::string build_system_prompt(const std::vector &knowledge p += "You are Nitro, an agentic AI assistant for software development.\n" "Your sandbox (project directory) is: " + sandbox + "\n\n" "## Tool protocol\n" - "Emit tool calls on their own line. The host executes them and returns\n" - "TOOL_RESULT: on the next line.\n\n" + " - Emit tool calls on their own new line. for example:\n\n" + "TOOL:LIST\n" + " - The host executes the tool and returns TOOL_RESULT: on the next line.\n\n" "Available tools:\n" " TOOL:LIST [dir] list files (default: sandbox root)\n" " TOOL:READ read file contents\n" @@ -1923,7 +1947,7 @@ static std::string build_system_prompt(const std::vector &knowledge "- Never access files outside the sandbox.\n" "- Use TOOL:PERMISSION before destructive or irreversible operations.\n" "- Use TOOL:CURL to fetch documentation, APIs, or web content you need.\n" - "- Reason step-by-step inside <|think|>… (hidden from user).\n" + "- Reason step-by-step inside <|think|> (hidden from user).\n" "- After each tool call, explain what you did in plain English.\n\n"; for (const auto &kf : knowledge_files) { std::ifstream f(kf); @@ -2080,8 +2104,7 @@ static void handle_slash(const std::string &input, else if (key == "n_gpu_layers") { cfg.n_gpu_layers = std::stoi(val); tui.append_line("[sys] n_gpu_layers will take effect on next /model load."); - } - else if (key == "run_allowed") { + } else if (key == "run_allowed") { // Accept a comma-separated list of basenames, or "none" to clear. cfg.run_allowed.clear(); if (val != "none") { @@ -2100,8 +2123,10 @@ static void handle_slash(const std::string &input, for (const auto &e : cfg.run_allowed) list += e + " "; tui.append_line("[sys] run_allowed: " + list); } + } else { + tui.append_line("[err] Unknown key '" + key + "'. Try /help for list."); + ok = false; } - else { tui.append_line("[err] Unknown key '" + key + "'. Try /help for list."); ok = false; } } catch (const std::exception &ex) { tui.append_line(std::string("[err] /set: ") + ex.what()); ok = false; @@ -2246,22 +2271,30 @@ int main(int argc, char **argv) { tui.redraw_all(); } + // log_open(); + // ── Main loop ───────────────────────────────────────────────────── for (;;) { { unsigned rows = 0, cols = 0; notcurses_stddim_yx(tui.nc, &rows, &cols); - if ((int)rows != tui.term_rows || (int)cols != tui.term_cols) + if ((int)rows != tui.term_rows || (int)cols != tui.term_cols) { tui.resize(); + } } std::string input = tui.readline_blocking(); input.erase(0, input.find_first_not_of(" \t")); - if (!input.empty()) + if (!input.empty()) { input.erase(input.find_last_not_of(" \t\r\n") + 1); - if (input.empty()) continue; + } + if (input.empty()) { + continue; + } tui.append_line("You: " + input); tui.redraw_all(); - if (input == "exit" || input == "quit") break; + if (input == "exit" || input == "quit") { + break; + } if (input[0] == '/') { handle_slash(input, cfg, agent, tui); } else { @@ -2269,6 +2302,8 @@ int main(int argc, char **argv) { } } + // log_close(); + tui.destroy(); // Persist input history for the next session. tui.history.save(history_path()); From 7ecac603036610e981fda5ce18f0a535d833051f Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Mon, 25 May 2026 21:16:08 +0930 Subject: [PATCH 117/131] LLAMA: fix nitro tool handling --- llama/nitro.cpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/llama/nitro.cpp b/llama/nitro.cpp index bdff9ea..2496a02 100644 --- a/llama/nitro.cpp +++ b/llama/nitro.cpp @@ -1446,8 +1446,8 @@ bool AgentState::run_turn(const std::string &user_message, tui.set_thinking(false); std::string buffer; - auto invoke_tool = [&](const std::string &buffer, const std::string_view template_str) -> void { - std::string result = process_tool(buffer, cfg, tui); + auto invoke_tool = [&](const std::string &tool, const std::string_view template_str) -> void { + std::string result = process_tool(tool, cfg, tui); std::string content = std::vformat(template_str, std::make_format_args(result)); if (!llama->add_message(*iter, "tool_result", content)) { tui.append_line(std::string("[err] tool result inject: ") + llama->last_error()); @@ -1486,7 +1486,6 @@ bool AgentState::run_turn(const std::string &user_message, while (iter->_has_next) { std::string tok = llama->next(*iter); buffer += tok; - if (think_mode == t_init) { start_think(""); start_think("<|think|>"); @@ -1502,7 +1501,7 @@ bool AgentState::run_turn(const std::string &user_message, } if (think_mode == t_thunk) { auto tool_start = buffer.find("TOOL:"); - if (tool_start != std::string::npos) { + if (tool_start == 0) { // fetch all remaining tokens invoke_tool(buffer + llama->all(*iter), "TOOL_RESULT: {}"); buffer.clear(); @@ -1510,8 +1509,8 @@ bool AgentState::run_turn(const std::string &user_message, continue; } auto pos = buffer.find('\n'); - if (pos != std::string::npos && pos > 0) { - tui.append_token(buffer.substr(0, pos) + "\n"); + if (pos != std::string::npos) { + tui.append_token(buffer.substr(0, pos + 1)); buffer = buffer.substr(pos + 1); } } @@ -1945,6 +1944,7 @@ static std::string build_system_prompt(const std::vector &knowledge " TOOL:CURL HTTP GET; returns response body (max 32 KB)\n\n" "Rules:\n" "- Never access files outside the sandbox.\n" + "- Only use one TOOL at a time. Never combine, always use each tool step by step\n" "- Use TOOL:PERMISSION before destructive or irreversible operations.\n" "- Use TOOL:CURL to fetch documentation, APIs, or web content you need.\n" "- Reason step-by-step inside <|think|> (hidden from user).\n" @@ -2200,6 +2200,8 @@ int main(int argc, char **argv) { cfg.embed_path = resolve_path(take_next(a.c_str())); } else if (a == "-g" || a == "--gpu-layers") { cfg.n_gpu_layers = std::stoi(take_next(a.c_str())); + } else if (a == "-l" || a == "--log") { + log_open(); } else if (a == "-h" || a == "--help") { std::puts("Usage: nitro [options] [project_dir]\n" "\n" @@ -2207,6 +2209,7 @@ int main(int argc, char **argv) { " -m, --model GGUF model to load on startup\n" " -e, --embed embedding model for RAG\n" " -g, --gpu-layers GPU layers to offload (default: 32)\n" + " -l, --log enabled logging\n" " -h, --help show this help\n" "\n" "project_dir defaults to the current working directory.\n" @@ -2271,8 +2274,6 @@ int main(int argc, char **argv) { tui.redraw_all(); } - // log_open(); - // ── Main loop ───────────────────────────────────────────────────── for (;;) { { @@ -2302,8 +2303,7 @@ int main(int argc, char **argv) { } } - // log_close(); - + log_close(); tui.destroy(); // Persist input history for the next session. tui.history.save(history_path()); From a1fc6260ff06b0516a585628ff1384211bd8a856 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Wed, 27 May 2026 12:53:05 +0930 Subject: [PATCH 118/131] LLAMA: nitro - partial fix for RAG handling --- llama/CMakeLists.txt | 13 +- llama/llama-sb-rag.cpp | 418 ++++++++++++++++++++++++++++++++--------- llama/llama-sb-rag.h | 20 ++ llama/llama-sb.cpp | 48 +++++ llama/llama-sb.h | 14 +- llama/llama.cpp | 2 +- llama/nitro.cpp | 82 +++++--- 7 files changed, 458 insertions(+), 139 deletions(-) diff --git a/llama/CMakeLists.txt b/llama/CMakeLists.txt index ef68b0f..562ba3f 100644 --- a/llama/CMakeLists.txt +++ b/llama/CMakeLists.txt @@ -113,7 +113,6 @@ add_subdirectory(${LLAMA_DIR}) set(PLUGIN_SOURCES main.cpp llama-sb.cpp - llama-sb-rag.cpp ../include/param.cpp ../include/hashmap.cpp ../include/apiexec.cpp @@ -237,6 +236,7 @@ if(NC_FOUND) message(STATUS "notcurses found β€” building nitro") add_executable(nitro nitro.cpp + llama-sb-rag.cpp ) target_include_directories(nitro PRIVATE ${LLAMA_DIR}/include @@ -262,17 +262,6 @@ else() message(STATUS "notcurses not found β€” skipping nitro (set -DNOTCURSES_DIR=... to enable)") endif() -# ----------------------------- -# Header preparation for RAG indexer -# ----------------------------- -add_executable(chunk_headers - chunk_headers.cpp -) - -set_target_properties(chunk_headers PROPERTIES - RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin -) - # ------------------------------------------------------------------ # Android native library # ------------------------------------------------------------------ diff --git a/llama/llama-sb-rag.cpp b/llama/llama-sb-rag.cpp index a64f31f..4db4a9d 100644 --- a/llama/llama-sb-rag.cpp +++ b/llama/llama-sb-rag.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -18,117 +19,235 @@ #include #include -bool Llama::embed_text(const std::string &text, std::vector &out, int embed_dim) { - vector tokens = tokenize(text); - if (tokens.size() == 0) { - return false; - } - - // truncate to context window - int n_ctx = llama_n_ctx(_ctx); - int n = tokens.size(); - if (n > n_ctx) { - _last_error = std::format("warning: chunk truncated {} -> {} tokens ", n, n_ctx); - n = n_ctx; - tokens.resize(n); +namespace fs = std::filesystem; + +static constexpr uint32_t MAGIC = 0x52414744; +static constexpr size_t MIN_CHUNK = 40; +static constexpr const char *INSTRUCT_EMBED = "Instruct: Represent this API documentation for code retrieval\nQuery: "; +static constexpr const char *INSTRUCT_QUERY = "Instruct: Given a programming question, retrieve relevant API documentation\nQuery: "; + +enum class ChunkType { + Function, Struct, Enum, Typedef, Defines, Other +}; + +static std::string type_name(ChunkType t) { + switch (t) { + case ChunkType::Function: return "function"; + case ChunkType::Struct: return "struct"; + case ChunkType::Enum: return "enum"; + case ChunkType::Typedef: return "typedef"; + case ChunkType::Defines: return "defines"; + default: return "other"; } +} - llama_memory_clear(llama_get_memory(_ctx), true); +/* ── helpers ───────────────────────────────────────────────── */ - if (!batch_decode_tokens(tokens)) { - return false; - } - - float *emb = llama_get_embeddings_seq(_ctx, 0); - if (!emb) { - emb = llama_get_embeddings_ith(_ctx, n - 1); - } +static bool starts_with(const std::string &s, const std::string &prefix) { + return s.size() >= prefix.size() && + s.compare(0, prefix.size(), prefix) == 0; +} - if (!emb) { - _last_error = "no embedding returned\n"; - return false; - } +static bool is_blank(const std::string &s) { + for (char c : s) if (!isspace((unsigned char)c)) return false; + return true; +} - out.assign(emb, emb + embed_dim); +/* ── state machine ─────────────────────────────────────────── */ - /* L2 normalize */ - float norm = 0.0f; - for (float v : out) { - norm += v * v; - } - norm = std::sqrt(norm); - if (norm > 1e-9f) { - for (float &v : out) { - v /= norm; - } - } +enum class State { + Idle, BlockComment, LineComment, Declaration, Struct, Defines +}; - return true; -} +template -bool Llama::rag_load(RagDB &db, const std::string &path) { - std::ifstream f(path, std::ios::binary); +static bool chunk_file(const fs::path &path, EmitChunk emit_chunk) { + std::ifstream f(path); if (!f) { - _last_error = std::format("rag_load: cannot open {}", path); return false; } - auto read32 = [&]() -> uint32_t { - uint32_t v = 0; f.read((char*)&v, 4); return v; - }; - auto read16 = [&]() -> uint16_t { - uint16_t v = 0; f.read((char*)&v, 2); return v; - }; - auto read8 = [&]() -> uint8_t { - uint8_t v = 0; f.read((char*)&v, 1); return v; - }; - auto readstr = [&](size_t len) -> std::string { - std::string s(len, '\0'); - f.read(&s[0], (std::streamsize)len); - return s; + const std::string source = path.filename().string(); + + State state = State::Idle; + std::string chunk; + ChunkType chunk_type = ChunkType::Other; + int brace_depth = 0; + int paren_depth = 0; + int define_count = 0; + + auto flush = [&](ChunkType t) { + emit_chunk(source, t, chunk); + chunk.clear(); + state = State::Idle; + brace_depth = 0; + paren_depth = 0; }; - uint32_t magic = read32(); - uint32_t version = read32(); - uint32_t n = read32(); - uint32_t edim = read32(); + std::string line; + while (std::getline(f, line)) { + /* trim trailing CR */ + if (!line.empty() && line.back() == '\r') line.pop_back(); + + /* find first non-whitespace for prefix checks */ + size_t trim_pos = 0; + while (trim_pos < line.size() && + (line[trim_pos] == ' ' || line[trim_pos] == '\t')) ++trim_pos; + const std::string trimmed = line.substr(trim_pos); + + /* ── #define handling ─────────────────────────────────── */ + if (starts_with(trimmed, "#define ")) { + if (state == State::BlockComment || state == State::LineComment) { + chunk += line + "\n"; + state = State::Defines; + define_count = 1; + } else if (state == State::Defines) { + chunk += line + "\n"; + define_count++; + } else { + if (chunk.size() >= MIN_CHUNK) emit_chunk(source, chunk_type, chunk); + chunk.clear(); + chunk += line + "\n"; + state = State::Defines; + define_count = 1; + } + continue; + } - if (magic != 0x52414744) { - _last_error = "rag_load: bad magic"; - return false; - } - if (version != 2) { - _last_error = std::format("rag_load: unsupported version {} (expected 2)", version); - return false; - } + /* non-define while in define group */ + if (state == State::Defines) { + flush(ChunkType::Defines); + define_count = 0; + /* fall through to process this line normally */ + } - db.embed_dim = (int)edim; - db.chunks.resize(n); + /* ── block comment start ──────────────────────────────── */ + if ((starts_with(trimmed, "/*") || starts_with(trimmed, "/**")) && + state == State::Idle) { + if (chunk.size() >= MIN_CHUNK) emit_chunk(source, chunk_type, chunk); + chunk.clear(); + chunk_type = ChunkType::Other; + chunk += line + "\n"; + state = (trimmed.find("*/", 2) != std::string::npos) + ? State::LineComment + : State::BlockComment; + continue; + } - for (uint32_t i = 0; i < n; i++) { - RagChunk &c = db.chunks[i]; + /* ── inside block comment ─────────────────────────────── */ + if (state == State::BlockComment) { + chunk += line + "\n"; + if (trimmed.find("*/") != std::string::npos) + state = State::LineComment; + continue; + } - uint32_t text_len = read32(); - c.text = readstr(text_len); + /* ── // line comment ──────────────────────────────────── */ + if (starts_with(trimmed, "//")) { + if (state == State::Idle) { + if (chunk.size() >= MIN_CHUNK) emit_chunk(source, chunk_type, chunk); + chunk.clear(); + chunk += line + "\n"; + state = State::LineComment; + } else if (state == State::LineComment) { + chunk += line + "\n"; + } + continue; + } - uint16_t src_len = read16(); - c.source = readstr(src_len); + /* ── blank line ───────────────────────────────────────── */ + if (is_blank(trimmed)) { + if (state == State::LineComment) + flush(ChunkType::Other); + else if (state == State::Idle && chunk.size() >= MIN_CHUNK) + flush(chunk_type); + continue; + } - uint8_t type_len = read8(); - c.type = readstr(type_len); + /* ── skip preprocessor noise ──────────────────────────── */ + if (starts_with(trimmed, "#ifndef") || starts_with(trimmed, "#ifdef") || + starts_with(trimmed, "#endif") || starts_with(trimmed, "#pragma") || + starts_with(trimmed, "#include")) { + if (state == State::LineComment || state == State::BlockComment) { + chunk.clear(); + state = State::Idle; + } + continue; + } - c.embedding.resize(edim); - f.read((char*)c.embedding.data(), (std::streamsize)(edim * sizeof(float))); - } + /* ── typedef struct / enum start ─────────────────────── */ + if ((starts_with(trimmed, "typedef struct") || + starts_with(trimmed, "typedef enum") || + starts_with(trimmed, "struct ") || + starts_with(trimmed, "enum ")) && + (state == State::Idle || state == State::LineComment)) { + + if (state == State::Idle && chunk.size() >= MIN_CHUNK) + emit_chunk(source, chunk_type, chunk); + + /* preserve any comment already in chunk */ + if (state == State::Idle) chunk.clear(); + + chunk += line + "\n"; + chunk_type = starts_with(trimmed, "typedef") ? ChunkType::Typedef + : starts_with(trimmed, "enum ") ? ChunkType::Enum + : ChunkType::Struct; + state = State::Struct; + for (char c : line) { + if (c == '{') ++brace_depth; + if (c == '}') --brace_depth; + } + if (brace_depth <= 0 && line.find(';') != std::string::npos) + flush(chunk_type); + continue; + } - if (!f) { - _last_error = "rag_load: read error"; - return false; + /* ── inside struct/enum body ──────────────────────────── */ + if (state == State::Struct) { + chunk += line + "\n"; + for (char c : line) { + if (c == '{') ++brace_depth; + if (c == '}') --brace_depth; + } + if (brace_depth <= 0 && line.find(';') != std::string::npos) + flush(chunk_type); + continue; + } + + /* ── function / other declaration ────────────────────── */ + if (state == State::LineComment || state == State::Idle) { + if (state == State::Idle && chunk.size() >= MIN_CHUNK) { + emit_chunk(source, chunk_type, chunk); + chunk.clear(); + } + chunk += line + "\n"; + chunk_type = ChunkType::Function; + state = State::Declaration; + for (char c : line) { + if (c == '(') ++paren_depth; + if (c == ')') --paren_depth; + } + if (paren_depth <= 0 && line.find(';') != std::string::npos) + flush(ChunkType::Function); + continue; + } + + /* ── multi-line declaration ───────────────────────────── */ + if (state == State::Declaration) { + chunk += line + "\n"; + for (char c : line) { + if (c == '(') ++paren_depth; + if (c == ')') --paren_depth; + } + if (paren_depth <= 0 && line.find(';') != std::string::npos) + flush(ChunkType::Function); + continue; + } } - std::cerr << "rag: loaded " << db.chunks.size() - << " chunks (dim=" << db.embed_dim - << ") from " << path << "\n"; + /* flush remainder */ + if (chunk.size() >= MIN_CHUNK) emit_chunk(source, chunk_type, chunk); + return true; } @@ -162,6 +281,29 @@ static std::string rag_build_context(const RagDB &db, return out.str(); } +// +// index the file +// +bool Llama::rag_index(RagDB &db, const std::string &filepath) { + bool embed_fail = false; + auto emit_chunk = [&](const std::string &source, ChunkType type, + const std::string &text) { + if (text.size() > MIN_CHUNK) { + RagChunk chunk; + chunk.text = text; + chunk.source = source; + chunk.type = type_name(type); + if (!embed_text(INSTRUCT_EMBED + text, chunk.embedding, db.embed_dim)) { + embed_fail = true; + } else { + db.chunks.push_back(std::move(chunk)); + } + } + }; + + return !embed_fail && chunk_file(filepath, emit_chunk); +} + // // retrieve with session // @@ -174,7 +316,7 @@ std::string Llama::rag_retrieve(const RagDB &db, } std::vector qvec; - std::string text = "Instruct: Given a programming question, retrieve relevant API documentation\nQuery: " + query; + std::string text = INSTRUCT_QUERY + query; if (!embed_text(text, qvec, db.embed_dim)) { return {}; } @@ -183,11 +325,10 @@ std::string Llama::rag_retrieve(const RagDB &db, std::vector order(db.size()); std::iota(order.begin(), order.end(), 0); std::vector scores(db.size()); - for (int i = 0; i < db.size(); i++) + for (int i = 0; i < db.size(); i++) { scores[i] = rag_cosine(qvec, db.chunks[i].embedding); - - std::sort(order.begin(), order.end(), - [&](int a, int b){ return scores[a] > scores[b]; }); + } + std::sort(order.begin(), order.end(), [&](int a, int b){ return scores[a] > scores[b]; }); // collect top_k unseen, within budget, above threshold std::vector result_idx; @@ -207,3 +348,96 @@ std::string Llama::rag_retrieve(const RagDB &db, return rag_build_context(db, result_idx, result_scores); } + +bool RagDB::save(const std::string &path) { + std::ofstream f(path, std::ios::binary); + if (!f) { + return false; + } + + auto write32 = [&](uint32_t v) { f.write((char*)&v, 4); }; + auto write16 = [&](uint16_t v) { f.write((char*)&v, 2); }; + auto write8 = [&](uint8_t v) { f.write((char*)&v, 1); }; + auto writestr = [&](const std::string &s, size_t max_len) { + size_t len = std::min(s.size(), max_len); + f.write(s.c_str(), (std::streamsize)len); + }; + + write32(MAGIC); /* magic "RAGD" */ + write32(2); /* version */ + write32((uint32_t)chunks.size()); /* n_chunks */ + write32((uint32_t)embed_dim); /* embed_dim */ + + for (const RagChunk &c : chunks) { + write32((uint32_t)c.text.size()); + f.write(c.text.c_str(), (std::streamsize)c.text.size()); + + uint16_t src_len = (uint16_t)std::min(c.source.size(), (size_t)65535); + write16(src_len); + writestr(c.source, src_len); + + uint8_t type_len = (uint8_t)std::min(c.type.size(), (size_t)255); + write8(type_len); + writestr(c.type, type_len); + + f.write((char*)c.embedding.data(), + (std::streamsize)(embed_dim * sizeof(float))); + } + + return f.good(); +} + +bool RagDB::load(const std::string &path) { + std::ifstream f(path, std::ios::binary); + if (!f) { + return false; + } + + auto read32 = [&]() -> uint32_t { + uint32_t v = 0; f.read((char*)&v, 4); return v; + }; + auto read16 = [&]() -> uint16_t { + uint16_t v = 0; f.read((char*)&v, 2); return v; + }; + auto read8 = [&]() -> uint8_t { + uint8_t v = 0; f.read((char*)&v, 1); return v; + }; + auto readstr = [&](size_t len) -> std::string { + std::string s(len, '\0'); + f.read(&s[0], (std::streamsize)len); + return s; + }; + + uint32_t magic = read32(); + uint32_t version = read32(); + uint32_t n = read32(); + uint32_t edim = read32(); + + if (magic != MAGIC) { + return false; + } + if (version != 2) { + return false; + } + + embed_dim = (int)edim; + chunks.resize(n); + + for (uint32_t i = 0; i < n; i++) { + RagChunk &c = chunks[i]; + + uint32_t text_len = read32(); + c.text = readstr(text_len); + + uint16_t src_len = read16(); + c.source = readstr(src_len); + + uint8_t type_len = read8(); + c.type = readstr(type_len); + + c.embedding.resize(edim); + f.read((char*)c.embedding.data(), (std::streamsize)(edim * sizeof(float))); + } + + return true; +} diff --git a/llama/llama-sb-rag.h b/llama/llama-sb-rag.h index d31706c..0296f26 100644 --- a/llama/llama-sb-rag.h +++ b/llama/llama-sb-rag.h @@ -14,10 +14,30 @@ struct RagChunk { std::vector embedding; }; +/* ── on-disk chunk (variable-length text) ──────────────────── */ +/* + * db header (16 bytes): + * uint32 magic = 0x52414744 "RAGD" + * uint32 version = 2 + * uint32 n_chunks + * uint32 embed_dim + * + * per chunk: + * uint32 text_len + * char[] text (text_len bytes, no null) + * uint16 source_len + * char[] source (source_len bytes, no null) + * uint8 type_len + * char[] type (type_len bytes, no null) + * float[] embedding (embed_dim floats) + */ struct RagDB { std::vector chunks; int embed_dim = 0; + bool load(const std::string &path); + bool save(const std::string &path); + int size() const { return (int)chunks.size(); } bool empty() const { return chunks.empty(); } }; diff --git a/llama/llama-sb.cpp b/llama/llama-sb.cpp index a4ad885..83e881f 100644 --- a/llama/llama-sb.cpp +++ b/llama/llama-sb.cpp @@ -417,6 +417,54 @@ LlamaMemoryInfo Llama::memory_info() { return info; } +bool Llama::embed_text(const std::string &text, std::vector &out, int embed_dim) { + vector tokens = tokenize(text); + if (tokens.size() == 0) { + return false; + } + + // truncate to context window + int n_ctx = llama_n_ctx(_ctx); + int n = tokens.size(); + if (n > n_ctx) { + _last_error = std::format("warning: chunk truncated {} -> {} tokens ", n, n_ctx); + n = n_ctx; + tokens.resize(n); + } + + llama_memory_clear(llama_get_memory(_ctx), true); + + if (!batch_decode_tokens(tokens)) { + return false; + } + + float *emb = llama_get_embeddings_seq(_ctx, 0); + if (!emb) { + emb = llama_get_embeddings_ith(_ctx, n - 1); + } + + if (!emb) { + _last_error = "no embedding returned\n"; + return false; + } + + out.assign(emb, emb + embed_dim); + + /* L2 normalize */ + float norm = 0.0f; + for (float v : out) { + norm += v * v; + } + norm = std::sqrt(norm); + if (norm > 1e-9f) { + for (float &v : out) { + v /= norm; + } + } + + return true; +} + bool Llama::batch_decode_tokens(vector &tokens) { uint32_t n_batch = llama_n_batch(_ctx); for (size_t i = 0; i < tokens.size(); i += n_batch) { diff --git a/llama/llama-sb.h b/llama/llama-sb.h index 43b7c7c..02e2359 100644 --- a/llama/llama-sb.h +++ b/llama/llama-sb.h @@ -101,12 +101,18 @@ struct Llama { // memory info LlamaMemoryInfo memory_info(); - // rag support - bool embed_text(const std::string &text, std::vector &out, int embed); - int get_embed_dim() const { return _model != nullptr ? llama_model_n_embd(_model) : 0; } - bool rag_load(RagDB &db, const std::string &path); + // creates an embedding vector of the given dimension for the given text + bool embed_text(const std::string &text, std::vector &out, int embed_dim); + + // retrieves rag query context informatiion from the rag database std::string rag_retrieve(const RagDB &db, const std::string &query, int top_k, RagSession &session); + // indexes the details from the given file + bool rag_index(RagDB &db, const std::string &filepath); + + // returns the emdedding dimension for the loaded model + int get_embed_dim() const { return _model != nullptr ? llama_model_n_embd(_model) : 0; } + private: bool batch_decode_tokens(vector &tokens); bool configure_sampler(); diff --git a/llama/llama.cpp b/llama/llama.cpp index 6db1304..dbe9c0c 160000 --- a/llama/llama.cpp +++ b/llama/llama.cpp @@ -1 +1 @@ -Subproject commit 6db130445d29b243ee2171efb8cd61b84a1c5322 +Subproject commit dbe9c0c8ce65354c372f5d4ab507e5424a755e9f diff --git a/llama/nitro.cpp b/llama/nitro.cpp index 2496a02..79b2ebe 100644 --- a/llama/nitro.cpp +++ b/llama/nitro.cpp @@ -34,7 +34,6 @@ // TOOL:DATE // TOOL:TIME // TOOL:RND -// TOOL:PERMISSION // TOOL:CURL // // Copyright (C) 2026 Chris Warren-Smith β€” GPLv2 or later @@ -209,7 +208,7 @@ class InputHistory { } /** - * @brief Load history from ~/.config/nitro.history (one entry per line). + * @brief Load history from ~/.config/nitro/nitro.history (one entry per line). * Silently succeeds if the file doesn't exist. */ void load(const std::string &path) { @@ -260,7 +259,7 @@ static FILE *g_logfile = nullptr; static void log_open() { const char *home = getenv("HOME"); - std::string path = std::string(home ? home : ".") + "/.config/nitro.log"; + std::string path = std::string(home ? home : ".") + "/.config/nitro/nitro.log"; g_logfile = fopen(path.c_str(), "a"); } @@ -285,7 +284,7 @@ static void log_write(const char *fmt, ...) { } // ═══════════════════════════════════════════════════════════════════════════ -// Settings persistence (~/.config/nitro.settings.json) +// Settings persistence (~/.config/nitro/nitro.settings.json) // ═══════════════════════════════════════════════════════════════════════════ // A minimal hand-rolled JSON reader/writer for the flat key-value settings // we care about. We deliberately avoid a full JSON library dependency. @@ -312,18 +311,18 @@ struct NitroConfig { std::vector run_allowed; }; -// Returns the canonical settings path: ~/.config/nitro.settings.json +// Returns the canonical settings path: ~/.config/nitro/settings.json static std::string settings_path() { const char *home = getenv("HOME"); std::string base = home ? std::string(home) : "."; - return base + "/.config/nitro.settings.json"; + return base + "/.config/nitro/settings.json"; } -// Returns the history file path: ~/.config/nitro.history +// Returns the history file path: ~/.config/nitro/history.txt static std::string history_path() { const char *home = getenv("HOME"); std::string base = home ? std::string(home) : "."; - return base + "/.config/nitro.history"; + return base + "/.config/nitro/history.txt"; } // Tiny helper: extract a quoted string value from flat JSON for a known key. @@ -464,10 +463,9 @@ static std::string introspect(const NitroConfig &cfg) { cfg.rag_top_k); } -// Persist the current cfg to ~/.config/nitro.settings.json. +// Persist the current cfg to ~/.config/nitro/settings.json. static bool save_settings(const NitroConfig &cfg) { std::string path = settings_path(); - // Ensure ~/.config/ exists fs::path dir = fs::path(path).parent_path(); std::error_code ec; fs::create_directories(dir, ec); @@ -562,7 +560,7 @@ struct TuiState { void append_token(const std::string &token); void flush_token_acc(); // ── interaction ─────────────────────────────────────────────────── - void confirm_dialog(const std::string &prompt, std::string &result); + bool confirm_dialog(const std::string &prompt); // Blocking readline with history navigation, cursor, arrow-key scrolling. std::string readline_blocking(); // Modal popup overlay while a long operation runs. @@ -573,11 +571,11 @@ struct TuiState { void show_modal_popup(const std::string &message); void show_help(); void dismiss_modal_popup(); - // ── RAG folder picker popup ─────────────────────────────────────── + // ── folder picker popup ─────────────────────────────────────── // Presents an interactive directory browser to let the user choose a // folder (or file) to index. Returns the selected path, or empty string // if the user cancelled. - // ── RAG / file browser popup ───────────────────────────────────── + // ── file browser popup ───────────────────────────────────── // Used by /rag, /model, and /embed to pick a path interactively. // Pass a hint string shown in the title bar (e.g. "RAG Folder", // "Model File", "Embedding Model"). @@ -898,7 +896,7 @@ void TuiState::dismiss_modal_popup() { } } -// ─── TuiState::rag_folder_picker ────────────────────────────────────────── +// ─── TuiState::file_picker ──────────────────────────────────────────────── // Interactive directory/file browser popup. // Keyboard: ↑/↓ navigate, Enter select/descend, Backspace go up, // 's' select current dir for indexing, Esc cancel. @@ -1099,7 +1097,7 @@ std::string TuiState::file_picker(const std::string &start_dir, } // ─── TuiState::confirm_dialog ───────────────────────────────────────────── -void TuiState::confirm_dialog(const std::string &prompt, std::string &result) { +bool TuiState::confirm_dialog(const std::string &prompt) { ncplane_erase(inputpl); ncplane_set_channels(inputpl, inp_ch(255, 200, 80)); std::string msg = " " + prompt + " [y/n] ❯ "; @@ -1119,9 +1117,9 @@ void TuiState::confirm_dialog(const std::string &prompt, std::string &result) { } std::string lo = answer; std::transform(lo.begin(), lo.end(), lo.begin(), ::tolower); - result = (lo == "y" || lo == "yes" || lo == "sure" || lo == "k") ? "YES" : "NO"; redraw_input(); notcurses_render(nc); + return (lo == "y" || lo == "yes" || lo == "sure" || lo == "k"); } // ─── TuiState::readline_blocking ────────────────────────────────────────── @@ -1389,23 +1387,31 @@ bool AgentState::rag_index(const std::string &path, TuiState &tui) { tui.redraw_all(); return false; } + auto index_one = [&](const std::string &filepath) { tui.append_line("[sys] indexing: " + filepath); tui.redraw_all(); - if (!embed_llama->rag_load(*rag_db, filepath)) { + if (!embed_llama->rag_index(*rag_db, filepath)) { tui.append_line(std::string("[err] rag_load: ") + embed_llama->last_error()); tui.redraw_all(); } }; + + // must be set before indexing + rag_db->embed_dim = embed_llama->get_embed_dim(); + fs::path rp(path); std::error_code ec; if (fs::is_directory(rp, ec)) { for (const auto &entry : fs::recursive_directory_iterator(rp, ec)) { - if (entry.is_regular_file()) index_one(entry.path().string()); + if (entry.is_regular_file()) { + index_one(entry.path().string()); + } } } else { index_one(path); } + return true; } @@ -1503,7 +1509,24 @@ bool AgentState::run_turn(const std::string &user_message, auto tool_start = buffer.find("TOOL:"); if (tool_start == 0) { // fetch all remaining tokens - invoke_tool(buffer + llama->all(*iter), "TOOL_RESULT: {}"); + invoke_tool(trim(buffer + llama->all(*iter)), "TOOL_RESULT: {}"); + buffer.clear(); + think_mode = t_init; + continue; + } + // see https://ai.google.dev/gemma/docs/core/prompt-formatting-gemma4 + tool_start = buffer.find("<|tool_call>call:"); + if (tool_start != std::string::npos) { + buffer += llama->all(*iter); + auto pos = buffer.find_last_not_of("}"); + if (pos != std::string::npos) { + buffer = buffer.substr(0, pos); + } + pos = buffer.find_first_not_of("{"); + if (pos != std::string::npos) { + buffer = buffer.substr(0, pos) + buffer.substr(pos + 1); + } + invoke_tool(trim(buffer), "<|tool_response>{}"); buffer.clear(); think_mode = t_init; continue; @@ -1869,15 +1892,14 @@ static std::string process_tool(const std::string &cmd, const NitroConfig &cfg, } if (op == "TOOL:WRITE") { std::string p = resolve(arg1); - if (!path_in_sandbox(sandbox, p)) return "ERROR: path outside sandbox"; + if (!path_in_sandbox(sandbox, p)) { + return "ERROR: path outside sandbox"; + } + if (!tui.confirm_dialog(std::format("Allow model to write {}?", p))) { + return "ERROR: action prevented by user"; + } std::string content = strip_code_fences(arg1, arg2); - return write_file(p, content) ? "OK: written to " + arg1 - : "ERROR: write failed for " + arg1; - } - if (op == "TOOL:PERMISSION") { - std::string result; - tui.confirm_dialog("Allow model to proceed?", result); - return result; + return write_file(p, content) ? "OK: written to " + arg1 : "ERROR: write failed for " + arg1; } if (op == "TOOL:CURL") { return tool_curl(arg1); @@ -1898,6 +1920,8 @@ static std::string process_tool(const std::string &cmd, const NitroConfig &cfg, return "ERROR: '" + basename + "' is not in the TOOL:RUN allowlist. " "Use /set run_allowed to permit it."; } + } else if (!tui.confirm_dialog(std::format("Allow {} to run?", prog))) { + return "ERROR: prevented by user"; } std::string command = prog + " " + arg2 + " 2>&1"; FILE *fp = popen(command.c_str(), "r"); @@ -1939,13 +1963,11 @@ static std::string build_system_prompt(const std::vector &knowledge " TOOL:DATE current date\n" " TOOL:TIME current time\n" " TOOL:RND random float\n" - " TOOL:PERMISSION ask user for explicit permission\n" " TOOL:INTROSPECT introspect your settings, top_k etc\n" " TOOL:CURL HTTP GET; returns response body (max 32 KB)\n\n" "Rules:\n" "- Never access files outside the sandbox.\n" "- Only use one TOOL at a time. Never combine, always use each tool step by step\n" - "- Use TOOL:PERMISSION before destructive or irreversible operations.\n" "- Use TOOL:CURL to fetch documentation, APIs, or web content you need.\n" "- Reason step-by-step inside <|think|> (hidden from user).\n" "- After each tool call, explain what you did in plain English.\n\n"; @@ -2213,7 +2235,7 @@ int main(int argc, char **argv) { " -h, --help show this help\n" "\n" "project_dir defaults to the current working directory.\n" - "Settings are persisted to ~/.config/nitro.settings.json.\n" + "Settings are persisted to ~/.config/nitro/settings.json.\n" "\n" "Slash commands inside nitro:\n" " /model [path] load / hot-reload a GGUF (picker if no path)\n" From 4fd29cc426a25b8a003f2f1df67ab8f458df4339 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Wed, 27 May 2026 16:56:38 +0930 Subject: [PATCH 119/131] LLAMA: nitro - refactoring, added logging, escape handling --- llama/nitro.cpp | 400 ++++++++++++++++++++++-------------------------- 1 file changed, 186 insertions(+), 214 deletions(-) diff --git a/llama/nitro.cpp b/llama/nitro.cpp index 79b2ebe..088ceaf 100644 --- a/llama/nitro.cpp +++ b/llama/nitro.cpp @@ -72,81 +72,31 @@ static std::string strip_code_fences(const std::string &filename, const std::st static std::string process_tool(const std::string &line, const NitroConfig &cfg, TuiState &tui); static std::string build_system_prompt(const std::vector &knowledge_files, const std::string &sandbox); -// ─── RAG indexing ───────────────────────────────────────────────────────────── -static constexpr int BATCH_SIZE = 512; - -struct Chunk { - std::string text; - std::string source; - std::string type; - std::vector embedding; +// ═══════════════════════════════════════════════════════════════════════════ +// NitroConfig +// ═══════════════════════════════════════════════════════════════════════════ +struct NitroConfig { + std::string model_path; + std::string embed_path; + std::string sandbox; + int n_ctx = 65536; + int n_batch = 512; + int n_gpu_layers = 32; + int n_max_tokens = 4096; + int log_level = GGML_LOG_LEVEL_CONT; + float temperature = 0.6f; + float top_p = 0.95f; + float min_p = 0.0f; + int top_k = 20; + float penalty_repeat = 1.0f; + int penalty_last_n = 256; + std::vector knowledge_files; + int rag_top_k = 5; + // TOOL:RUN allowlist β€” if non-empty, only these program basenames may run. + // Empty means "allow anything inside the sandbox" (original behaviour). + std::vector run_allowed; }; -static bool json_get_string(const std::string &json, - const std::string &key, - std::string &out) { - std::string search = "\"" + key + "\":"; - size_t pos = json.find(search); - if (pos == std::string::npos) return false; - pos += search.size(); - while (pos < json.size() && json[pos] == ' ') ++pos; - if (pos >= json.size() || json[pos] != '"') return false; - ++pos; - out.clear(); - while (pos < json.size()) { - char c = json[pos++]; - if (c == '\\' && pos < json.size()) { - char e = json[pos++]; - switch (e) { - case 'n': out += '\n'; break; - case 't': out += '\t'; break; - case '"': out += '"'; break; - case '\\': out += '\\'; break; - default: out += e; break; - } - } else if (c == '"') { - break; - } else { - out += c; - } - } - return true; -} - -static bool save_db(const std::string &path, - const std::vector &chunks, - int embed_dim) { - std::ofstream f(path, std::ios::binary); - if (!f) { - std::fprintf(stderr, "cannot open for write: %s\n\n", path); - return false; - } - auto write32 = [&](uint32_t v) { f.write((char*)&v, 4); }; - auto write16 = [&](uint16_t v) { f.write((char*)&v, 2); }; - auto write8 = [&](uint8_t v) { f.write((char*)&v, 1); }; - auto writestr = [&](const std::string &s, size_t max_len) { - size_t len = std::min(s.size(), max_len); - f.write(s.c_str(), (std::streamsize)len); - }; - write32(0x52414744); - write32(2); - write32((uint32_t)chunks.size()); - write32((uint32_t)embed_dim); - for (const Chunk &c : chunks) { - write32((uint32_t)c.text.size()); - f.write(c.text.c_str(), (std::streamsize)c.text.size()); - uint16_t src_len = (uint16_t)std::min(c.source.size(), (size_t)65535); - write16(src_len); - writestr(c.source, src_len); - uint8_t type_len = (uint8_t)std::min(c.type.size(), (size_t)255); - write8(type_len); - writestr(c.type, type_len); - f.write((char*)c.embedding.data(), - (std::streamsize)(embed_dim * sizeof(float))); - } - return f.good(); -} - // ═══════════════════════════════════════════════════════════════════════════ // InputHistory β€” up/down arrow navigation through submitted inputs // ═══════════════════════════════════════════════════════════════════════════ @@ -251,6 +201,121 @@ class InputHistory { int current_index = 0; }; +// ═══════════════════════════════════════════════════════════════════════════ +// Notcurses TUI +// ═══════════════════════════════════════════════════════════════════════════ +// +// β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ header (1 row) ─────────────────────────────────┐ +// β”‚ ✦ NITRO model: … tok/s: … KV: …% VRAM: …% β”‚ +// β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +// β”‚ β”‚ +// β”‚ chat pane (rows 1 … term_rows-3) β”‚ +// β”‚ β”‚ +// β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +// β”‚ ───────────────────────────────────── (separator) β”‚ +// β”‚ ❯ input β”‚ +// β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +struct TuiState { + // ── notcurses handles ────────────────────────────────────────────── + struct notcurses *nc = nullptr; + struct ncplane *stdpl = nullptr; + struct ncplane *header = nullptr; + struct ncplane *chatpl = nullptr; + struct ncplane *inputpl = nullptr; + // ── chat buffer ─────────────────────────────────────────────────── + std::vector chat_lines; + int scroll_offset = 0; + std::mutex lines_mutex; + // ── streaming accumulator ───────────────────────────────────────── + std::string token_acc; + // ── input ───────────────────────────────────────────────────────── + std::string input_buf; + size_t cursor_pos = 0; + bool mouse_mode = true; + // ── status bar values ───────────────────────────────────────────── + std::string current_model = "none"; + float tokens_per_sec = 0.0f; + int kv_used = 0; + int kv_total = 1; + size_t vram_used = 0; + size_t vram_total = 1; + int term_rows = 0; + int term_cols = 0; + // ── thinking spinner ────────────────────────────────────────────── + bool thinking = false; + int spinner_frame = 0; + // ── input history ───────────────────────────────────────────────── + InputHistory history; + // Advance spinner by one frame and redraw the header. + void tick_spinner(); + // Toggle thinking mode; redraws header immediately. + void set_thinking(bool on); + // ── lifecycle ───────────────────────────────────────────────────── + void init(); + void destroy(); + void resize(); + // ── draw ────────────────────────────────────────────────────────── + void redraw_header(); + void redraw_chat(); + void redraw_input(); + void redraw_all(); + // ── content helpers ─────────────────────────────────────────────── + void append_line(const std::string &line); + void append_token(const std::string &token); + void flush_token_acc(); + // ── interaction ─────────────────────────────────────────────────── + bool confirm_dialog(const std::string &prompt); + // Blocking readline with history navigation, cursor, arrow-key scrolling. + std::string readline_blocking(); + // Modal popup overlay while a long operation runs. + // Call show_modal_popup to display; dismiss_modal_popup to remove. + // The popup plane is stored in modal_plane; callers hold it as an opaque + // handle β€” or just use the paired helpers below. + struct ncplane *modal_plane = nullptr; + void show_modal_popup(const std::string &message); + void show_help(); + void dismiss_modal_popup(); + // ── folder picker popup ─────────────────────────────────────── + // Presents an interactive directory browser to let the user choose a + // folder (or file) to index. Returns the selected path, or empty string + // if the user cancelled. + // ── file browser popup ───────────────────────────────────── + // Used by /rag, /model, and /embed to pick a path interactively. + // Pass a hint string shown in the title bar (e.g. "RAG Folder", + // "Model File", "Embedding Model"). + // Returns the selected path, or empty string if the user cancelled. + std::string file_picker(const std::string &start_dir, + const std::string &title_hint = "File"); + // Legacy alias kept for callers that used the old name. + std::string rag_folder_picker(const std::string &start_dir) { + return file_picker(start_dir, "RAG Folder"); + } +}; + +// ═══════════════════════════════════════════════════════════════════════════ +// AgentState +// ═══════════════════════════════════════════════════════════════════════════ +struct AgentState { + std::unique_ptr llama; + std::unique_ptr iter; + std::unique_ptr embed_llama; + std::unique_ptr rag_db; + std::unique_ptr rag_session; + bool model_loaded = false; + std::string system_prompt; + + bool setup_model(const NitroConfig &cfg, TuiState &tui); + bool setup_embed(const std::string &path, TuiState &tui); + void apply_generation_params(const NitroConfig &cfg); + void reset_conversation(const std::string &sysprompt, TuiState &tui); + bool run_turn(const std::string &user_message, + const NitroConfig &cfg, + TuiState &tui); + bool rag_index(const std::string &path, TuiState &tui); + std::string memory_info_text(); + float tokens_per_sec() const; +}; + // ═══════════════════════════════════════════════════════════════════════════ // Logging // ═══════════════════════════════════════════════════════════════════════════ @@ -289,28 +354,6 @@ static void log_write(const char *fmt, ...) { // A minimal hand-rolled JSON reader/writer for the flat key-value settings // we care about. We deliberately avoid a full JSON library dependency. -struct NitroConfig { - std::string model_path; - std::string embed_path; - std::string sandbox; - int n_ctx = 65536; - int n_batch = 512; - int n_gpu_layers = 32; - int n_max_tokens = 4096; - int log_level = GGML_LOG_LEVEL_CONT; - float temperature = 0.6f; - float top_p = 0.95f; - float min_p = 0.0f; - int top_k = 20; - float penalty_repeat = 1.0f; - int penalty_last_n = 256; - std::vector knowledge_files; - int rag_top_k = 5; - // TOOL:RUN allowlist β€” if non-empty, only these program basenames may run. - // Empty means "allow anything inside the sandbox" (original behaviour). - std::vector run_allowed; -}; - // Returns the canonical settings path: ~/.config/nitro/settings.json static std::string settings_path() { const char *home = getenv("HOME"); @@ -325,6 +368,37 @@ static std::string history_path() { return base + "/.config/nitro/history.txt"; } +static bool json_get_string(const std::string &json, + const std::string &key, + std::string &out) { + std::string search = "\"" + key + "\":"; + size_t pos = json.find(search); + if (pos == std::string::npos) return false; + pos += search.size(); + while (pos < json.size() && json[pos] == ' ') ++pos; + if (pos >= json.size() || json[pos] != '"') return false; + ++pos; + out.clear(); + while (pos < json.size()) { + char c = json[pos++]; + if (c == '\\' && pos < json.size()) { + char e = json[pos++]; + switch (e) { + case 'n': out += '\n'; break; + case 't': out += '\t'; break; + case '"': out += '"'; break; + case '\\': out += '\\'; break; + default: out += e; break; + } + } else if (c == '"') { + break; + } else { + out += c; + } + } + return true; +} + // Tiny helper: extract a quoted string value from flat JSON for a known key. static bool settings_get_str(const std::string &json, const std::string &key, @@ -497,97 +571,6 @@ std::string trim(std::string_view str) { return std::string(str.substr(start, end - start + 1)); } -// ═══════════════════════════════════════════════════════════════════════════ -// Notcurses TUI -// ═══════════════════════════════════════════════════════════════════════════ -// -// β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ header (1 row) ─────────────────────────────────┐ -// β”‚ ✦ NITRO model: … tok/s: … KV: …% VRAM: …% β”‚ -// β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -// β”‚ β”‚ -// β”‚ chat pane (rows 1 … term_rows-3) β”‚ -// β”‚ β”‚ -// β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -// β”‚ ───────────────────────────────────── (separator) β”‚ -// β”‚ ❯ input β”‚ -// β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -struct TuiState { - // ── notcurses handles ────────────────────────────────────────────── - struct notcurses *nc = nullptr; - struct ncplane *stdpl = nullptr; - struct ncplane *header = nullptr; - struct ncplane *chatpl = nullptr; - struct ncplane *inputpl = nullptr; - // ── chat buffer ─────────────────────────────────────────────────── - std::vector chat_lines; - int scroll_offset = 0; - std::mutex lines_mutex; - // ── streaming accumulator ───────────────────────────────────────── - std::string token_acc; - // ── input ───────────────────────────────────────────────────────── - std::string input_buf; - size_t cursor_pos = 0; - bool mouse_mode = true; - // ── status bar values ───────────────────────────────────────────── - std::string current_model = "none"; - float tokens_per_sec = 0.0f; - int kv_used = 0; - int kv_total = 1; - size_t vram_used = 0; - size_t vram_total = 1; - int term_rows = 0; - int term_cols = 0; - // ── thinking spinner ────────────────────────────────────────────── - bool thinking = false; - int spinner_frame = 0; - // ── input history ───────────────────────────────────────────────── - InputHistory history; - // Advance spinner by one frame and redraw the header. - void tick_spinner(); - // Toggle thinking mode; redraws header immediately. - void set_thinking(bool on); - // ── lifecycle ───────────────────────────────────────────────────── - void init(); - void destroy(); - void resize(); - // ── draw ────────────────────────────────────────────────────────── - void redraw_header(); - void redraw_chat(); - void redraw_input(); - void redraw_all(); - // ── content helpers ─────────────────────────────────────────────── - void append_line(const std::string &line); - void append_token(const std::string &token); - void flush_token_acc(); - // ── interaction ─────────────────────────────────────────────────── - bool confirm_dialog(const std::string &prompt); - // Blocking readline with history navigation, cursor, arrow-key scrolling. - std::string readline_blocking(); - // Modal popup overlay while a long operation runs. - // Call show_modal_popup to display; dismiss_modal_popup to remove. - // The popup plane is stored in modal_plane; callers hold it as an opaque - // handle β€” or just use the paired helpers below. - struct ncplane *modal_plane = nullptr; - void show_modal_popup(const std::string &message); - void show_help(); - void dismiss_modal_popup(); - // ── folder picker popup ─────────────────────────────────────── - // Presents an interactive directory browser to let the user choose a - // folder (or file) to index. Returns the selected path, or empty string - // if the user cancelled. - // ── file browser popup ───────────────────────────────────── - // Used by /rag, /model, and /embed to pick a path interactively. - // Pass a hint string shown in the title bar (e.g. "RAG Folder", - // "Model File", "Embedding Model"). - // Returns the selected path, or empty string if the user cancelled. - std::string file_picker(const std::string &start_dir, - const std::string &title_hint = "File"); - // Legacy alias kept for callers that used the old name. - std::string rag_folder_picker(const std::string &start_dir) { - return file_picker(start_dir, "RAG Folder"); - } -}; - // ─── colour helpers ────────────────────────────────────────────────────── static constexpr uint32_t BG_CHAT_R = 18, BG_CHAT_G = 22, BG_CHAT_B = 30; static constexpr uint32_t BG_INP_R = 22, BG_INP_G = 28, BG_INP_B = 38; @@ -1205,7 +1188,7 @@ std::string TuiState::readline_blocking() { notcurses_render(nc); continue; } - if (ni.id == NCKEY_SCROLL_DOWN && scroll_offset > 1) { + if (ni.id == NCKEY_SCROLL_DOWN && scroll_offset > 0) { scroll_offset -= 1; redraw_chat(); notcurses_render(nc); @@ -1250,30 +1233,6 @@ std::string TuiState::readline_blocking() { } } -// ═══════════════════════════════════════════════════════════════════════════ -// AgentState -// ═══════════════════════════════════════════════════════════════════════════ -struct AgentState { - std::unique_ptr llama; - std::unique_ptr iter; - std::unique_ptr embed_llama; - std::unique_ptr rag_db; - std::unique_ptr rag_session; - bool model_loaded = false; - std::string system_prompt; - - bool setup_model(const NitroConfig &cfg, TuiState &tui); - bool setup_embed(const std::string &path, TuiState &tui); - void apply_generation_params(const NitroConfig &cfg); - void reset_conversation(const std::string &sysprompt, TuiState &tui); - bool run_turn(const std::string &user_message, - const NitroConfig &cfg, - TuiState &tui); - bool rag_index(const std::string &path, TuiState &tui); - std::string memory_info_text(); - float tokens_per_sec() const; -}; - void AgentState::apply_generation_params(const NitroConfig &cfg) { // llama->add_stop(MARKER_END_TOOL); llama->add_stop("<|turn|>"); @@ -1428,10 +1387,12 @@ bool AgentState::run_turn(const std::string &user_message, } std::string effective_message = user_message; if (embed_llama && rag_db && rag_session) { - std::string context = llama->rag_retrieve(*rag_db, user_message, - cfg.rag_top_k, *rag_session); + std::string context = embed_llama->rag_retrieve(*rag_db, user_message, cfg.rag_top_k, *rag_session); if (!context.empty()) { + log_write("RAG: %s", context.c_str()); effective_message = "Context:\n" + context + "\n\nUser: " + user_message; + } else { + log_write("RAG: no context found [%s]", embed_llama->last_error()); } } if (!iter) { @@ -1490,6 +1451,14 @@ bool AgentState::run_turn(const std::string &user_message, }; while (iter->_has_next) { + ncinput ni{}; + notcurses_get_nblock(tui.nc, &ni); + if (ni.id == NCKEY_ESC) { + tui.set_thinking(false); + tui.append_line("[err] Generation cancelled by user (Escape)"); + tui.redraw_all(); + return false; + } std::string tok = llama->next(*iter); buffer += tok; if (think_mode == t_init) { @@ -1521,7 +1490,7 @@ bool AgentState::run_turn(const std::string &user_message, auto pos = buffer.find_last_not_of("}"); if (pos != std::string::npos) { buffer = buffer.substr(0, pos); - } + } pos = buffer.find_first_not_of("{"); if (pos != std::string::npos) { buffer = buffer.substr(0, pos) + buffer.substr(pos + 1); @@ -2280,6 +2249,8 @@ int main(int argc, char **argv) { tui.history.load(history_path()); welcome(tui, cfg.sandbox); + log_write("nitro starting"); + // ── Init agent ──────────────────────────────────────────────────── AgentState agent; if (!cfg.model_path.empty()) { @@ -2325,6 +2296,7 @@ int main(int argc, char **argv) { } } + log_write("nitro exiting"); log_close(); tui.destroy(); // Persist input history for the next session. From dd53542d7db4a239c1368d09d9b0b5a01e8a7772 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Wed, 27 May 2026 19:30:37 +0930 Subject: [PATCH 120/131] LLAMA: nitro - code cleanup added RAG tool --- llama/llama-sb-rag.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/llama/llama-sb-rag.cpp b/llama/llama-sb-rag.cpp index 4db4a9d..0b11d04 100644 --- a/llama/llama-sb-rag.cpp +++ b/llama/llama-sb-rag.cpp @@ -312,12 +312,14 @@ std::string Llama::rag_retrieve(const RagDB &db, int top_k, RagSession &session) { if (db.empty()) { + _last_error = "no input"; return {}; } std::vector qvec; std::string text = INSTRUCT_QUERY + query; if (!embed_text(text, qvec, db.embed_dim)) { + _last_error = "failed to embed text"; return {}; } From 4dab858533fd9422f71be544316ff4befa252b0e Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Wed, 27 May 2026 20:42:33 +0930 Subject: [PATCH 121/131] LLAMA: nitro - load and save rag index files --- llama/llama.cpp | 2 +- llama/nitro.cpp | 393 +++++++++++++++++++++++++++--------------------- 2 files changed, 225 insertions(+), 170 deletions(-) diff --git a/llama/llama.cpp b/llama/llama.cpp index dbe9c0c..4d8cc0c 160000 --- a/llama/llama.cpp +++ b/llama/llama.cpp @@ -1 +1 @@ -Subproject commit dbe9c0c8ce65354c372f5d4ab507e5424a755e9f +Subproject commit 4d8cc0c56ffba3f8b7fdb0130627fed2a6f71958 diff --git a/llama/nitro.cpp b/llama/nitro.cpp index 088ceaf..bd97714 100644 --- a/llama/nitro.cpp +++ b/llama/nitro.cpp @@ -57,24 +57,23 @@ namespace fs = std::filesystem; -// ═══════════════════════════════════════════════════════════════════════════ +// // Forward declarations -// ═══════════════════════════════════════════════════════════════════════════ +// struct NitroConfig; struct TuiState; struct AgentState; -static std::string join_path(const std::string &a, const std::string &b); -static std::string read_file(const std::string &path); +static bool path_in_sandbox(const std::string &sandbox, const std::string &path); static bool write_file(const std::string &path, const std::string &data); +static std::string build_system_prompt(const std::vector &knowledge_files, const std::string &sandbox); +static std::string join_path(const std::string &a, const std::string &b); static std::string list_dir(const std::string &path); -static bool path_in_sandbox(const std::string &sandbox, const std::string &path); +static std::string read_file(const std::string &path); static std::string strip_code_fences(const std::string &filename, const std::string &src); -static std::string process_tool(const std::string &line, const NitroConfig &cfg, TuiState &tui); -static std::string build_system_prompt(const std::vector &knowledge_files, const std::string &sandbox); - -// ═══════════════════════════════════════════════════════════════════════════ +static std::string tool_curl(const std::string &url); +// // NitroConfig -// ═══════════════════════════════════════════════════════════════════════════ +// struct NitroConfig { std::string model_path; std::string embed_path; @@ -97,9 +96,9 @@ struct NitroConfig { std::vector run_allowed; }; -// ═══════════════════════════════════════════════════════════════════════════ +// // InputHistory β€” up/down arrow navigation through submitted inputs -// ═══════════════════════════════════════════════════════════════════════════ +// class InputHistory { public: explicit InputHistory() = default; @@ -201,9 +200,9 @@ class InputHistory { int current_index = 0; }; -// ═══════════════════════════════════════════════════════════════════════════ +// // Notcurses TUI -// ═══════════════════════════════════════════════════════════════════════════ +// // // β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ header (1 row) ─────────────────────────────────┐ // β”‚ ✦ NITRO model: … tok/s: … KV: …% VRAM: …% β”‚ @@ -292,9 +291,9 @@ struct TuiState { } }; -// ═══════════════════════════════════════════════════════════════════════════ +// // AgentState -// ═══════════════════════════════════════════════════════════════════════════ +// struct AgentState { std::unique_ptr llama; std::unique_ptr iter; @@ -304,21 +303,23 @@ struct AgentState { bool model_loaded = false; std::string system_prompt; - bool setup_model(const NitroConfig &cfg, TuiState &tui); + bool rag_index(const std::string &path, const NitroConfig &cfg, TuiState &tui); + bool rag_load_index(const std::string &path, TuiState &tui); + bool run_turn(const std::string &user_message, const NitroConfig &cfg, TuiState &tui); bool setup_embed(const std::string &path, TuiState &tui); + bool setup_model(const NitroConfig &cfg, TuiState &tui); void apply_generation_params(const NitroConfig &cfg); void reset_conversation(const std::string &sysprompt, TuiState &tui); - bool run_turn(const std::string &user_message, - const NitroConfig &cfg, - TuiState &tui); - bool rag_index(const std::string &path, TuiState &tui); std::string memory_info_text(); + std::string process_tool(const std::string &cmd, const NitroConfig &cfg, TuiState &tui); + std::string rag_tool(const NitroConfig &cfg, const std::string &agent_query); float tokens_per_sec() const; }; -// ═══════════════════════════════════════════════════════════════════════════ +// // Logging -// ═══════════════════════════════════════════════════════════════════════════ +// + // ─── Debug logging (file-backed, safe to call while notcurses is active) ── static FILE *g_logfile = nullptr; @@ -348,9 +349,9 @@ static void log_write(const char *fmt, ...) { fflush(g_logfile); // flush immediately so tail -f works } -// ═══════════════════════════════════════════════════════════════════════════ +// // Settings persistence (~/.config/nitro/nitro.settings.json) -// ═══════════════════════════════════════════════════════════════════════════ +// // A minimal hand-rolled JSON reader/writer for the flat key-value settings // we care about. We deliberately avoid a full JSON library dependency. @@ -588,7 +589,9 @@ static inline uint64_t hdr_ch(uint32_t r, uint32_t g, uint32_t b) { return NCCHANNELS_INITIALIZER(r, g, b, BG_HDR_R, BG_HDR_G, BG_HDR_B); } -// ─── TuiState::init ────────────────────────────────────────────────────── +// +// TuiState::init +// void TuiState::init() { notcurses_options opts{}; opts.flags = NCOPTION_SUPPRESS_BANNERS; @@ -637,7 +640,9 @@ void TuiState::resize() { redraw_all(); } -// ─── TuiState::redraw_* ────────────────────────────────────────────────── +// +// TuiState::redraw +// void TuiState::redraw_header() { ncplane_erase(header); ncplane_set_base(header, " ", 0, @@ -751,7 +756,9 @@ void TuiState::set_thinking(bool on) { notcurses_render(nc); } -// ─── TuiState content helpers ───────────────────────────────────────────── +// +// TuiState content helpers +// void TuiState::append_line(const std::string &line) { std::lock_guard lk(lines_mutex); int w = std::max(1, term_cols - 1); @@ -787,9 +794,10 @@ void TuiState::flush_token_acc() { } } -// ─── TuiState::show_modal_popup / dismiss_modal_popup ───────────────────── +// // Creates a centred floating plane with a border and a status message. // The popup sits above all other planes and blocks until explicitly dismissed. +// void TuiState::show_modal_popup(const std::string &message) { // Dismiss any previous popup first. dismiss_modal_popup(); @@ -879,6 +887,7 @@ void TuiState::dismiss_modal_popup() { } } +// // ─── TuiState::file_picker ──────────────────────────────────────────────── // Interactive directory/file browser popup. // Keyboard: ↑/↓ navigate, Enter select/descend, Backspace go up, @@ -896,6 +905,7 @@ void TuiState::dismiss_modal_popup() { // Esc cancel β†’ returns "" // // Returns the chosen path, or "" on cancel. +// std::string TuiState::file_picker(const std::string &start_dir, const std::string &title_hint) { std::string current_dir = start_dir; @@ -1079,7 +1089,9 @@ std::string TuiState::file_picker(const std::string &start_dir, return result; } +// // ─── TuiState::confirm_dialog ───────────────────────────────────────────── +// bool TuiState::confirm_dialog(const std::string &prompt) { ncplane_erase(inputpl); ncplane_set_channels(inputpl, inp_ch(255, 200, 80)); @@ -1105,9 +1117,10 @@ bool TuiState::confirm_dialog(const std::string &prompt) { return (lo == "y" || lo == "yes" || lo == "sure" || lo == "k"); } -// ─── TuiState::readline_blocking ────────────────────────────────────────── +// // Integrates InputHistory: Up/Down arrows navigate the history stack. // On submit the entry is pushed to history, and nav is reset. +// std::string TuiState::readline_blocking() { input_buf.clear(); cursor_pos = 0; @@ -1234,7 +1247,6 @@ std::string TuiState::readline_blocking() { } void AgentState::apply_generation_params(const NitroConfig &cfg) { - // llama->add_stop(MARKER_END_TOOL); llama->add_stop("<|turn|>"); llama->add_stop("<|im_end|>"); llama->set_max_tokens(cfg.n_max_tokens); @@ -1247,8 +1259,9 @@ void AgentState::apply_generation_params(const NitroConfig &cfg) { llama->set_log_level(cfg.log_level); } -// ─── AgentState::setup_model ────────────────────────────────────────────── +// // Shows a modal loading popup while the model loads. +// bool AgentState::setup_model(const NitroConfig &cfg, TuiState &tui) { if (cfg.model_path.empty()) { tui.append_line("[sys] No model loaded. Use /model to load a GGUF."); @@ -1340,7 +1353,35 @@ std::string AgentState::memory_info_text() { return oss.str(); } -bool AgentState::rag_index(const std::string &path, TuiState &tui) { +std::string AgentState::rag_tool(const NitroConfig &cfg, const std::string &agent_query) { + std::string result; + if (embed_llama && rag_db && rag_session) { + result = embed_llama->rag_retrieve(*rag_db, agent_query, cfg.rag_top_k, *rag_session); + if (result.empty()) { + result = "RAG: no context found"; + } + } else { + result = "RAG: not enabled"; + } + return result; +} + +bool AgentState::rag_load_index(const std::string &path, TuiState &tui) { + if (!embed_llama || !rag_db) { + tui.append_line("[err] Load an embedding model first: /embed "); + tui.redraw_all(); + return false; + } + + if (!rag_db->load(path)) { + tui.append_line("[sys] failed to load"); + tui.redraw_all(); + } + + return true; +} + +bool AgentState::rag_index(const std::string &path, const NitroConfig &cfg, TuiState &tui) { if (!embed_llama || !rag_db) { tui.append_line("[err] Load an embedding model first: /embed "); tui.redraw_all(); @@ -1371,15 +1412,135 @@ bool AgentState::rag_index(const std::string &path, TuiState &tui) { index_one(path); } + std::string save_path = join_path(cfg.sandbox, "rag-index.bin"); + tui.append_line("[sys] saving index: " + save_path); + tui.redraw_all(); + rag_db->save(save_path); + return true; } -// ═══════════════════════════════════════════════════════════════════════════ +// +// Tool dispatch +// +std::string AgentState::process_tool(const std::string &cmd, const NitroConfig &cfg, TuiState &tui) { + const std::string &sandbox = cfg.sandbox; + const std::vector &run_allowed = cfg.run_allowed; + + std::string op, arg1, arg2; + auto sp1 = cmd.find(' '); + if (sp1 == std::string::npos) { + op = trim(cmd); + } else { + op = trim(cmd.substr(0, sp1)); + std::string rest = cmd.substr(sp1 + 1); + rest.erase(0, rest.find_first_not_of(" \t")); + auto sp2 = rest.find(' '); + if (sp2 == std::string::npos) { + arg1 = rest; + } else { + arg1 = rest.substr(0, sp2); + arg2 = rest.substr(sp2 + 1); + } + } + + auto resolve = [&](const std::string &p) -> std::string { + if (p.empty() || p == ".") return sandbox; + if (p.substr(0, 2) == "./") return join_path(sandbox, p.substr(2)); + if (p[0] == '/') return p; + return join_path(sandbox, p); + }; + + tui.append_line("[tool] β†’ " + op); + tui.redraw_all(); + + if (op == "TOOL:DATE") { + char buf[32]; time_t t = time(nullptr); + strftime(buf, sizeof(buf), "%Y-%m-%d", localtime(&t)); + return buf; + } + if (op == "TOOL:TIME") { + char buf[32]; time_t t = time(nullptr); + strftime(buf, sizeof(buf), "%H:%M:%S", localtime(&t)); + return buf; + } + if (op == "TOOL:RND") { + return std::to_string((double)rand() / RAND_MAX); + } + if (op == "TOOL:RAG") { + return rag_tool(cfg, arg1); + } + if (op == "TOOL:LIST") { + std::string dir = resolve(arg1); + if (!path_in_sandbox(sandbox, dir)) return "ERROR: path outside sandbox"; + return list_dir(dir); + } + if (op == "TOOL:EXISTS") { + std::string p = resolve(arg1); + if (!path_in_sandbox(sandbox, p)) return "NO"; + return fs::exists(p) ? "YES" : "NO"; + } + if (op == "TOOL:READ") { + std::string p = resolve(arg1); + if (!path_in_sandbox(sandbox, p)) return "ERROR: path outside sandbox"; + return read_file(p); + } + if (op == "TOOL:WRITE") { + std::string p = resolve(arg1); + if (!path_in_sandbox(sandbox, p)) { + return "ERROR: path outside sandbox"; + } + if (!tui.confirm_dialog(std::format("Allow model to write {}?", p))) { + return "ERROR: action prevented by user"; + } + std::string content = strip_code_fences(arg1, arg2); + return write_file(p, content) ? "OK: written to " + arg1 : "ERROR: write failed for " + arg1; + } + if (op == "TOOL:CURL") { + return tool_curl(arg1); + } + if (op == "TOOL:INTROSPECT") { + return introspect(cfg); + } + if (op == "TOOL:RUN") { + std::string prog = resolve(arg1); + if (!path_in_sandbox(sandbox, prog)) { + return "ERROR: path outside sandbox"; + } + if (!run_allowed.empty()) { + std::string basename = fs::path(prog).filename().string(); + bool permitted = std::any_of(run_allowed.begin(), run_allowed.end(), + [&](const std::string &a){ return a == basename; }); + if (!permitted) { + return "ERROR: '" + basename + "' is not in the TOOL:RUN allowlist. " + "Use /set run_allowed to permit it."; + } + } else if (!tui.confirm_dialog(std::format("Allow {} to run?", prog))) { + return "ERROR: prevented by user"; + } + std::string command = prog + " " + arg2 + " 2>&1"; + FILE *fp = popen(command.c_str(), "r"); + if (!fp) { + return "ERROR: popen failed"; + } + std::string out; + char buf[256]; + while (fgets(buf, sizeof(buf), fp)) { + out += buf; + } + pclose(fp); + if (out.size() > 4096) { + out = out.substr(0, 4096) + "\n…(truncated)"; + } + return out; + } + return "ERROR: unknown tool: [" + op + "]"; +} + +// // Agent turn -// ═══════════════════════════════════════════════════════════════════════════ -bool AgentState::run_turn(const std::string &user_message, - const NitroConfig &cfg, - TuiState &tui) { +// +bool AgentState::run_turn(const std::string &user_message, const NitroConfig &cfg, TuiState &tui) { if (!model_loaded) { tui.append_line("[err] No model loaded. Use /model "); tui.redraw_all(); @@ -1530,9 +1691,9 @@ bool AgentState::run_turn(const std::string &user_message, return true; } -// ═══════════════════════════════════════════════════════════════════════════ +// // File-system helpers -// ═══════════════════════════════════════════════════════════════════════════ +// static std::string join_path(const std::string &a, const std::string &b) { if (b.empty()) return a; if (b[0] == '/') return b; @@ -1605,15 +1766,16 @@ static std::string strip_code_fences(const std::string &filename, return inner; } -// ═══════════════════════════════════════════════════════════════════════════ +// // html_to_text β€” strip HTML for cleaner TOOL:CURL context -// ═══════════════════════════════════════════════════════════════════════════ +// // Lightweight HTMLβ†’plain-text conversion: // β€’ Drops , and + for (const std::string &tag : {"script", "style"}) { + std::string open = "<" + tag; + std::string close = ""; + std::string lo = s; + std::transform(lo.begin(), lo.end(), lo.begin(), ::tolower); + for (;;) { + auto p0 = lo.find(open); + if (p0 == std::string::npos) break; + auto p1 = lo.find(close, p0); + if (p1 == std::string::npos) { s.erase(p0); lo.erase(p0); break; } + s.erase(p0, p1 + close.size() - p0); + lo.erase(p0, p1 + close.size() - p0); + } + } - // Message. - uint64_t msg_ch = NCCHANNELS_INITIALIZER(200, 200, 200, PBG_R, PBG_G, PBG_B); - ncplane_set_channels(modal_plane, msg_ch); - // Truncate message to fit inside border. - int max_msg = popup_w - 4; - std::string display = message.size() > (size_t)max_msg - ? message.substr(0, max_msg) - : message; - ncplane_putstr_yx(modal_plane, 2, 2, display.c_str()); + // 3. Replace block-level tags with '\n' before stripping all tags. + static const char *const BLOCK[] = { + "p","div","br","li","tr","h1","h2","h3","h4","h5","h6", + "article","section","header","footer","nav","main", nullptr + }; + { + std::string out; + out.reserve(s.size()); + size_t i = 0; + while (i < s.size()) { + if (s[i] != '<') { out += s[i++]; continue; } + auto ce = s.find('>', i); + if (ce == std::string::npos) { out += s[i++]; continue; } + std::string inner = s.substr(i + 1, ce - i - 1); + size_t sp = inner.find_first_of(" \t/\r\n"); + std::string name = (sp != std::string::npos) ? inner.substr(0, sp) : inner; + std::transform(name.begin(), name.end(), name.begin(), ::tolower); + for (int k = 0; BLOCK[k]; ++k) { + if (name == BLOCK[k]) { + out += '\n'; break; + } + } + i = ce + 1; + } + s = out; + } - notcurses_render(nc); -} + // 4. Strip all remaining tags. + { + std::string out; out.reserve(s.size()); + bool in_tag = false; + for (char c : s) { + if (c == '<') { in_tag = true; continue; } + if (c == '>') { in_tag = false; continue; } + if (!in_tag) out += c; + } + s = out; + } -void TuiState::show_help() { - append_line("[sys] Commands:"); - append_line("[sys] /model [path] load a GGUF model (picker if no path)"); - append_line("[sys] /embed [path] load an embedding model (picker if no path)"); - append_line("[sys] /rag [path] index file or directory (picker if no path)"); - append_line("[sys] /memory KV / VRAM / layer stats"); - append_line("[sys] /clear reset conversation"); - append_line("[sys] /settings show current settings"); - append_line("[sys] /set change a setting live"); - append_line("[sys] /help this message"); - append_line("[sys] exit / quit exit Nitro"); - append_line("[sys] Settable keys (via /set):"); - append_line("[sys] temperature top_p top_k min_p penalty_repeat"); - append_line("[sys] n_max_tokens penalty_last_n rag_top_k n_gpu_layers"); - append_line("[sys] run_allowed (comma-separated list, e.g. python3,make)"); - redraw_all(); -} + // 5. Decode common HTML entities. + static const std::pair ENT[] = { + {"&","&"},{"<","<"},{">",">"},{""","\""}, + {"'","'"},{" "," "},{"—","β€”"},{"–","–"}, + {"…","…"},{"'","'"},{""","\""}, + {nullptr,nullptr} + }; + for (int k = 0; ENT[k].first; ++k) { + std::string e = ENT[k].first, r = ENT[k].second; + size_t pos = 0; + while ((pos = s.find(e, pos)) != std::string::npos) + { s.replace(pos, e.size(), r); pos += r.size(); } + } + // Numeric entities &#NNN; and &#xHHH; + { + std::string out; out.reserve(s.size()); + size_t i = 0; + while (i < s.size()) { + if (s[i]=='&' && i+2>6)); out += (char)(0x80|(cp&0x3F)); } + else { out += (char)(0xE0|(cp>>12)); out += (char)(0x80|((cp>>6)&0x3F)); out += (char)(0x80|(cp&0x3F)); } + i = semi+1; continue; + } catch (...) {} + } + } + out += s[i++]; + } + s = out; + } -void TuiState::dismiss_modal_popup() { - if (modal_plane) { - ncplane_destroy(modal_plane); - modal_plane = nullptr; - notcurses_render(nc); + // 6. Collapse whitespace; cap blank lines at 2. + { + std::string out; out.reserve(s.size()); + int nl_run = 0; bool last_sp = false; + for (char c : s) { + if (c == '\r') continue; + if (c == '\t') c = ' '; + if (c == '\n') { ++nl_run; last_sp=false; if (nl_run<=2) out+='\n'; continue; } + nl_run = 0; + if (c == ' ') { if (!last_sp) { out+=' '; last_sp=true; } continue; } + last_sp = false; out += c; + } + size_t f = out.find_first_not_of(" \n"); + size_t l = out.find_last_not_of(" \n"); + s = (f == std::string::npos) ? "" : out.substr(f, l-f+1); } + return s; } -// -// ─── TuiState::file_picker ──────────────────────────────────────────────── -// Interactive directory/file browser popup. -// Keyboard: ↑/↓ navigate, Enter select/descend, Backspace go up, -// 's' select current dir for indexing, Esc cancel. -// Returns the chosen path or "" on cancel. -// ─── TuiState::file_picker ──────────────────────────────────────────────── -// Unified interactive directory/file browser used by /rag, /model, /embed. -// title_hint appears in the popup header (e.g. "RAG Folder", "Model File"). -// -// Keyboard: -// ↑/↓ navigate list -// Enter descend into directory, or select a file -// Backspace go up one directory -// s select the current directory itself (useful for /rag) -// Esc cancel β†’ returns "" -// -// Returns the chosen path, or "" on cancel. -// -std::string TuiState::file_picker(const std::string &start_dir, - const std::string &title_hint) { - std::string current_dir = start_dir; - { - std::error_code ec; - auto canon = fs::canonical(start_dir, ec); - if (!ec) current_dir = canon.string(); - } - auto load_entries = [](const std::string &dir, - std::vector &entries) { - entries.clear(); - std::error_code ec; - if (fs::path(dir).has_parent_path() && - fs::path(dir) != fs::path(dir).root_path()) - entries.push_back(".."); - std::vector dirs, files; - for (const auto &e : fs::directory_iterator(dir, ec)) { - if (ec) break; - std::string name = e.path().filename().string(); - if (name.empty() || name[0] == '.') continue; - if (e.is_directory()) dirs.push_back(name); - else files.push_back(name); - } - std::sort(dirs.begin(), dirs.end()); - std::sort(files.begin(), files.end()); - for (auto &d : dirs) entries.push_back(d + "/"); - for (auto &f : files) entries.push_back(f); - }; - - std::vector entries; - int selected = 0; - int scroll = 0; - - // Popup dimensions. - static constexpr int PW = 60; - static constexpr int PH = 20; - int py = std::max(0, (term_rows - PH) / 2); - int px = std::max(0, (term_cols - PW) / 2); +static std::string tool_curl(const std::string &url) { + if (url.empty()) return "ERROR: TOOL:CURL requires a URL argument"; + CURL *curl = curl_easy_init(); + if (!curl) return "ERROR: curl_easy_init failed"; + std::string body; + body.reserve(4096); + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_cb); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &body); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5L); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L); + curl_easy_setopt(curl, CURLOPT_USERAGENT, "nitro/1.0"); + // Accept compressed responses; curl will decompress automatically. + curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, ""); - ncplane_options opts{}; - opts.y = py; opts.x = px; - opts.rows = (unsigned)PH; opts.cols = (unsigned)PW; - struct ncplane *picker = ncplane_create(stdpl, &opts); - if (!picker) return ""; + CURLcode res = curl_easy_perform(curl); + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); - static constexpr uint32_t PBG_R = 18, PBG_G = 24, PBG_B = 40; - ncplane_set_base(picker, " ", 0, NCCHANNELS_INITIALIZER(PBG_R, PBG_G, PBG_B, PBG_R, PBG_G, PBG_B)); - // Build a compact hint line appropriate to the operation. - // /rag adds 's=select dir'; /model and /embed only need file selection. - std::string hint_line = "↑↓ navigate Enter open/select Esc cancel"; - if (title_hint.find("RAG") != std::string::npos || - title_hint.find("Folder") != std::string::npos) { - hint_line = "↑↓ navigate Enter open s=select dir Esc cancel"; + // Query content-type before cleanup (pointer is only valid while handle lives). + char *ct_raw = nullptr; + curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &ct_raw); + std::string content_type = ct_raw ? ct_raw : ""; + std::transform(content_type.begin(), content_type.end(), + content_type.begin(), ::tolower); + curl_easy_cleanup(curl); + if (res != CURLE_OK) { + return std::string("ERROR: curl: ") + curl_easy_strerror(res); + } + if (http_code >= 400) { + return "ERROR: HTTP " + std::to_string(http_code) + " from " + url; + } + if (body.empty()) { + return "(empty response)"; } - auto draw_picker = [&]() { - ncplane_erase(picker); - uint64_t border_ch = NCCHANNELS_INITIALIZER(100, 180, 255, PBG_R, PBG_G, PBG_B); - ncplane_set_channels(picker, border_ch); - ncplane_putstr_yx(picker, 0, 0, "β•”"); - for (int c = 1; c < PW - 1; ++c) ncplane_putstr_yx(picker, 0, c, "═"); - ncplane_putstr_yx(picker, 0, PW - 1, "β•—"); - for (int r = 1; r < PH - 1; ++r) { - ncplane_putstr_yx(picker, r, 0, "β•‘"); - ncplane_putstr_yx(picker, r, PW - 1, "β•‘"); - } - ncplane_putstr_yx(picker, PH - 1, 0, "β•š"); - for (int c = 1; c < PW - 1; ++c) ncplane_putstr_yx(picker, PH - 1, c, "═"); - ncplane_putstr_yx(picker, PH - 1, PW - 1, "╝"); - - // Title - ncplane_set_channels(picker, NCCHANNELS_INITIALIZER(255, 220, 80, PBG_R, PBG_G, PBG_B)); - std::string title_str = " πŸ“‚ " + title_hint + " Picker "; - if ((int)title_str.size() > PW - 4) title_str = title_str.substr(0, PW - 4); - ncplane_putstr_yx(picker, 0, 2, title_str.c_str()); - // Current path (truncated). - std::string path_display = current_dir; - if ((int)path_display.size() > PW - 4) - path_display = "…" + path_display.substr(path_display.size() - (PW - 5)); - ncplane_set_channels(picker, NCCHANNELS_INITIALIZER(160, 200, 240, PBG_R, PBG_G, PBG_B)); - ncplane_putstr_yx(picker, 1, 2, path_display.c_str()); - // Hint line (bottom interior row). - ncplane_set_channels(picker, NCCHANNELS_INITIALIZER(120, 120, 160, PBG_R, PBG_G, PBG_B)); - std::string hint_trunc = hint_line; - if ((int)hint_trunc.size() > PW - 4) hint_trunc = hint_trunc.substr(0, PW - 4); - ncplane_putstr_yx(picker, PH - 2, 2, hint_trunc.c_str()); - // Entry list. - int list_rows = PH - 5; - if (selected < scroll) scroll = selected; - if (selected >= scroll + list_rows) scroll = selected - list_rows + 1; - for (int i = 0; i < list_rows; ++i) { - int idx = scroll + i; - if (idx >= (int)entries.size()) break; - bool is_selected = (idx == selected); - bool is_dir = !entries[idx].empty() && entries[idx].back() == '/'; - uint32_t fr, fg, fb; - if (is_selected) { fr = 20; fg = 20; fb = 20; } - else if (is_dir) { fr = 120; fg = 200; fb = 255; } - else { fr = 200; fg = 200; fb = 200; } - uint32_t br = is_selected ? 100 : PBG_R; - uint32_t bg = is_selected ? 180 : PBG_G; - uint32_t bb = is_selected ? 255 : PBG_B; - ncplane_set_channels(picker, NCCHANNELS_INITIALIZER(fr, fg, fb, br, bg, bb)); - std::string label = (is_selected ? " β–Ά " : " ") + entries[idx]; - if ((int)label.size() > PW - 2) label = label.substr(0, PW - 2); - while ((int)label.size() < PW - 2) label += ' '; - ncplane_putstr_yx(picker, 2 + i, 1, label.c_str()); - } - notcurses_render(nc); - }; - - std::string result; - load_entries(current_dir, entries); - draw_picker(); - for (;;) { - ncinput ni{}; - notcurses_get_blocking(nc, &ni); - if (ni.id == NCKEY_ESC) { - break; // cancelled - } - if (ni.id == NCKEY_UP) { - if (selected > 0) --selected; - draw_picker(); - continue; - } - if (ni.id == NCKEY_DOWN) { - if (selected + 1 < (int)entries.size()) ++selected; - draw_picker(); - continue; - } - // 's' β€” select the current directory (useful for /rag, ignored for file pickers). - if (ni.id == 's' || ni.id == 'S') { - // Select current directory for RAG indexing. - result = current_dir; - break; - } - if (ni.id == NCKEY_BACKSPACE || ni.id == 127) { - // Go up one level. - fs::path p(current_dir); - if (p.has_parent_path() && p != p.root_path()) { - current_dir = p.parent_path().string(); - load_entries(current_dir, entries); - selected = 0; scroll = 0; - draw_picker(); - } - continue; - } - if (ni.id == NCKEY_ENTER || ni.id == '\r' || ni.id == '\n') { - if (entries.empty()) continue; - const std::string &entry = entries[selected]; - if (entry == "..") { - fs::path p(current_dir); - if (p.has_parent_path() && p != p.root_path()) { - current_dir = p.parent_path().string(); - load_entries(current_dir, entries); - selected = 0; scroll = 0; - draw_picker(); - } - } else if (!entry.empty() && entry.back() == '/') { - // Descend into directory. - current_dir = current_dir + "/" + entry.substr(0, entry.size() - 1); - { - std::error_code ec; - auto canon = fs::canonical(current_dir, ec); - if (!ec) current_dir = canon.string(); - } - load_entries(current_dir, entries); - selected = 0; scroll = 0; - draw_picker(); - } else { - // Select a specific file. - // Select the highlighted file. - result = current_dir + "/" + entry; - break; - } - continue; - } + // Strip HTML tags so the model receives clean plain text. + bool is_html = (content_type.find("text/html") != std::string::npos) + || (body.size() > 5 && body.substr(0,5) == " 6 && body.substr(0,6) == ""); + if (is_html) { + body = html_to_text(body); } - ncplane_destroy(picker); - notcurses_render(nc); - return result; + + return body; } // -// ─── TuiState::confirm_dialog ───────────────────────────────────────────── +// TuiState::init // -bool TuiState::confirm_dialog(const std::string &prompt) { - ncplane_erase(inputpl); - ncplane_set_channels(inputpl, inp_ch(255, 200, 80)); - std::string msg = " " + prompt + " [y/n] ❯ "; - ncplane_putstr_yx(inputpl, 1, 0, msg.c_str()); - notcurses_render(nc); - std::string answer; - for (;;) { - ncinput ni{}; - notcurses_get_blocking(nc, &ni); - if (ni.id == NCKEY_ENTER || ni.id == '\r' || ni.id == '\n') break; - if (ni.id == NCKEY_BACKSPACE && !answer.empty()) { answer.pop_back(); } - else if (ni.id >= 32 && ni.id < 127) { answer += (char)ni.id; } - ncplane_erase(inputpl); - ncplane_set_channels(inputpl, inp_ch(255, 200, 80)); - ncplane_putstr_yx(inputpl, 1, 0, (msg + answer).c_str()); - notcurses_render(nc); +void TuiState::init() { + notcurses_options opts{}; + opts.flags = NCOPTION_SUPPRESS_BANNERS; + nc = notcurses_init(&opts, nullptr); + if (!nc) { std::fputs("notcurses_init failed\n", stderr); std::exit(1); } + stdpl = notcurses_stdplane(nc); + notcurses_term_dim_yx(nc, (unsigned *)&term_rows, (unsigned *)&term_cols); + uint64_t bg = NCCHANNELS_INITIALIZER(BG_CHAT_R, BG_CHAT_G, BG_CHAT_B, + BG_CHAT_R, BG_CHAT_G, BG_CHAT_B); + ncplane_set_base(stdpl, " ", 0, bg); + ncplane_erase(stdpl); + ncplane_options hopt{}; + hopt.y = 0; hopt.x = 0; + hopt.rows = 1; hopt.cols = (unsigned)term_cols; + header = ncplane_create(stdpl, &hopt); + int chat_rows = std::max(1, term_rows - 3); + ncplane_options copt{}; + copt.y = 1; copt.x = 0; + copt.rows = (unsigned)chat_rows; copt.cols = (unsigned)term_cols; + chatpl = ncplane_create(stdpl, &copt); + ncplane_set_base(chatpl, " ", 0, + NCCHANNELS_INITIALIZER(BG_CHAT_R, BG_CHAT_G, BG_CHAT_B, + BG_CHAT_R, BG_CHAT_G, BG_CHAT_B)); + ncplane_options iopt{}; + iopt.y = term_rows - 2; iopt.x = 0; + iopt.rows = 2; iopt.cols = (unsigned)term_cols; + inputpl = ncplane_create(stdpl, &iopt); + ncplane_set_base(inputpl, " ", 0, + NCCHANNELS_INITIALIZER(BG_INP_R, BG_INP_G, BG_INP_B, + BG_INP_R, BG_INP_G, BG_INP_B)); + notcurses_mice_enable(nc, NCMICE_BUTTON_EVENT); + redraw_all(); +} + +void TuiState::destroy() { + if (nc) { + notcurses_stop(nc); + nc = nullptr; } - std::string lo = answer; - std::transform(lo.begin(), lo.end(), lo.begin(), ::tolower); - redraw_input(); - notcurses_render(nc); - return (lo == "y" || lo == "yes" || lo == "sure" || lo == "k"); +} + +void TuiState::resize() { + notcurses_term_dim_yx(nc, (unsigned *)&term_rows, (unsigned *)&term_cols); + ncplane_resize_simple(header, 1, (unsigned)term_cols); + int cr = std::max(1, term_rows - 3); + ncplane_resize_simple(chatpl, (unsigned)cr, (unsigned)term_cols); + ncplane_move_yx(inputpl, term_rows - 2, 0); + ncplane_resize_simple(inputpl, 2, (unsigned)term_cols); + redraw_all(); } // -// Integrates InputHistory: Up/Down arrows navigate the history stack. -// On submit the entry is pushed to history, and nav is reset. +// TuiState::redraw // -std::string TuiState::readline_blocking() { - input_buf.clear(); - cursor_pos = 0; - history.reset_nav(); - redraw_input(); - notcurses_render(nc); - - // Temporary saved draft so Down from history restores the user's current text. - std::string draft; - - for (;;) { - ncinput ni{}; - notcurses_get_blocking(nc, &ni); - - if (ni.id == NCKEY_ENTER || ni.id == '\r' || ni.id == '\n') { - std::string result = input_buf; - if (!result.empty()) { - history.push(result); - } - input_buf.clear(); - cursor_pos = 0; - redraw_input(); - notcurses_render(nc); - return result; - } - - if (ni.id == NCKEY_UP) { - // Entering history from a fresh prompt: save current text as draft. - std::string hist_entry; - if (history.up(hist_entry)) { - if (input_buf.size() > 0 && hist_entry != input_buf) { - // Only save draft when we first leave the bottom of history. - // (history.reset_nav was called on entry so the first Up call - // always comes from the "new input" position.) - draft = input_buf; - } - input_buf = hist_entry; - cursor_pos = input_buf.size(); - } - redraw_input(); - notcurses_render(nc); - continue; - } - - if (ni.id == NCKEY_DOWN) { - std::string hist_entry; - bool got = history.down(hist_entry); - if (got) { - input_buf = hist_entry; - cursor_pos = input_buf.size(); - } else { - // Past the newest entry β†’ restore draft. - input_buf = draft; - cursor_pos = input_buf.size(); - draft.clear(); - } - redraw_input(); - notcurses_render(nc); - continue; - } +void TuiState::redraw_header() { + ncplane_erase(header); + ncplane_set_base(header, " ", 0, + NCCHANNELS_INITIALIZER(BG_HDR_R, BG_HDR_G, BG_HDR_B, + BG_HDR_R, BG_HDR_G, BG_HDR_B)); + float kv_pct = kv_total > 0 ? 100.f * (float)kv_used / (float)kv_total : 0.f; + float vram_pct = vram_total > 0 ? 100.f * (float)vram_used / (float)vram_total : 0.f; + static const char *const SPIN[] = { "β£Ύ","β£½","β£»","β’Ώ","β‘Ώ","⣟","β£―","β£·" }; + const char *spin_str = thinking ? SPIN[spinner_frame % 8] : " "; + char buf[512]; + int n = std::snprintf(buf, sizeof(buf), + " ✦ NITRO β”‚ %-32s β”‚ %5.1f tok/s β”‚ KV %4.1f%% VRAM %4.1f%% %s", + current_model.c_str(), (double)tokens_per_sec, + (double)kv_pct, (double)vram_pct, spin_str); + if (n > term_cols) buf[term_cols] = '\0'; + ncplane_set_channels(header, hdr_ch(130, 220, 200)); + ncplane_putstr_yx(header, 0, 0, buf); +} - // Scroll the chat pane β€” not the input history. - if (ni.id == NCKEY_PGUP) { - scroll_offset += std::max(1, term_rows - 4); - redraw_chat(); - notcurses_render(nc); - continue; - } - if (ni.id == NCKEY_PGDOWN) { - scroll_offset = std::max(0, scroll_offset - std::max(1, term_rows - 4)); - redraw_chat(); - notcurses_render(nc); - continue; - } - if (ni.id == NCKEY_SCROLL_UP && scroll_offset < term_rows + 10) { - scroll_offset += 1; - redraw_chat(); - notcurses_render(nc); - continue; - } - if (ni.id == NCKEY_SCROLL_DOWN && scroll_offset > 0) { - scroll_offset -= 1; - redraw_chat(); - notcurses_render(nc); - continue; - } - if (ni.id == NCKEY_F01) { - show_help(); - continue; - } - if (ni.id == NCKEY_F02) { - mouse_mode = !mouse_mode; - if (mouse_mode) { - notcurses_mice_enable(nc, NCMICE_BUTTON_EVENT); - } else { - notcurses_mice_disable(nc); - } - continue; - } - if (ni.id == NCKEY_BACKSPACE || ni.id == 127) { - if (cursor_pos > 0) { input_buf.erase(cursor_pos - 1, 1); --cursor_pos; } - } else if (ni.id == NCKEY_LEFT) { - if (cursor_pos > 0) --cursor_pos; - } else if (ni.id == NCKEY_RIGHT) { - if (cursor_pos < input_buf.size()) ++cursor_pos; - } else if (ni.id == NCKEY_HOME) { - cursor_pos = 0; - } else if (ni.id == NCKEY_END) { - cursor_pos = input_buf.size(); - } else if (ni.id == NCKEY_DEL) { - if (cursor_pos < input_buf.size()) input_buf.erase(cursor_pos, 1); - } else if (ni.id >= 32 && ni.id < 0xD800) { - // Any printable character β€” entering new text clears the nav draft - // so that Down won't resurrect a stale saved buffer. - draft.clear(); - history.reset_nav(); - input_buf.insert(cursor_pos, 1, (char)ni.id); - ++cursor_pos; +void TuiState::redraw_chat() { + ncplane_erase(chatpl); + unsigned rows, cols; + ncplane_dim_yx(chatpl, &rows, &cols); + std::lock_guard lk(lines_mutex); + int total = (int)chat_lines.size(); + int visible = (int)rows; + int start = std::max(0, total - visible - scroll_offset); + int end = std::min(total, start + visible); + for (int i = start, row = 0; i < end; ++i, ++row) { + const std::string &line = chat_lines[i]; + uint64_t ch; + // Logo lines use prefix "[logo_N]" where N is the row index 0-6. + // We interpolate a cyanβ†’magenta gradient across the 7 art rows. + if (line.rfind("[logo_", 0) == 0 && line.size() > 7 && line[7] == ']') { + int logo_row = line[6] - '0'; + // Gradient: cyan (0,230,255) β†’ green (80,255,160) β†’ magenta (220,80,255) + // 7 steps, indices 0-6. + static const uint32_t GRAD_R[] = { 0, 20, 60, 120, 180, 210, 220 }; + static const uint32_t GRAD_G[] = { 230, 255, 255, 255, 200, 130, 80 }; + static const uint32_t GRAD_B[] = { 255, 200, 140, 80, 100, 200, 255 }; + int gi = std::max(0, std::min(logo_row, 6)); + ch = chat_ch(GRAD_R[gi], GRAD_G[gi], GRAD_B[gi]); } + else if (line.rfind("You: ", 0) == 0) ch = chat_ch(100, 200, 255); + else if (line.rfind("Nitro: ", 0) == 0) ch = chat_ch(180, 255, 180); + else if (line.rfind("[tool]", 0) == 0) ch = chat_ch(255, 180, 80); + else if (line.rfind("[err]", 0) == 0) ch = chat_ch(255, 80, 80); + else if (line.rfind("[sys]", 0) == 0) ch = chat_ch(140, 140, 200); + else ch = chat_ch(210, 210, 210); + ncplane_set_channels(chatpl, ch); + // Strip the [logo_N] prefix before rendering. + std::string display = (line.rfind("[logo_", 0) == 0 && line.size() > 8) + ? line.substr(8) : line; + if (display.size() > cols) display = display.substr(0, cols); + ncplane_putstr_yx(chatpl, row, 0, display.c_str()); + } +} - redraw_input(); - notcurses_render(nc); +void TuiState::redraw_input() { + ncplane_erase(inputpl); + ncplane_set_channels(inputpl, inp_ch(80, 120, 160)); + std::string sep(term_cols, '-'); + ncplane_putstr_yx(inputpl, 0, 0, sep.c_str()); + const std::string prompt = " ❯ "; + const int prompt_cols = 4; + ncplane_set_channels(inputpl, inp_ch(100, 210, 255)); + ncplane_putstr_yx(inputpl, 1, 0, prompt.c_str()); + int max_w = std::max(0, term_cols - prompt_cols - 1); + std::string visible = input_buf; + int view_offset = 0; + if ((int)visible.size() > max_w && max_w > 0) { + view_offset = (int)visible.size() - max_w; + visible = visible.substr(view_offset); + } + int cur_in_view = std::max(0, (int)cursor_pos - view_offset); + cur_in_view = std::min(cur_in_view, (int)visible.size()); + std::string before = visible.substr(0, cur_in_view); + std::string after = cur_in_view < (int)visible.size() + ? visible.substr(cur_in_view + 1) : ""; + char cursor_ch_val = cur_in_view < (int)visible.size() + ? visible[cur_in_view] : ' '; + ncplane_set_channels(inputpl, inp_ch(230, 230, 230)); + ncplane_putstr_yx(inputpl, 1, prompt_cols, before.c_str()); + int cx = prompt_cols + cur_in_view; + ncplane_set_channels(inputpl, NCCHANNELS_INITIALIZER(BG_INP_R, BG_INP_G, BG_INP_B, 180, 230, 255)); + char cbuf[2] = { cursor_ch_val, '\0' }; + ncplane_putstr_yx(inputpl, 1, cx, cbuf); + ncplane_set_channels(inputpl, inp_ch(230, 230, 230)); + if (!after.empty()) { + ncplane_putstr_yx(inputpl, 1, cx + 1, after.c_str()); } } -void AgentState::apply_generation_params(const NitroConfig &cfg) { - llama->add_stop("<|turn|>"); - llama->add_stop("<|im_end|>"); - llama->set_max_tokens(cfg.n_max_tokens); - llama->set_temperature(cfg.temperature); - llama->set_top_k(cfg.top_k); - llama->set_top_p(cfg.top_p); - llama->set_min_p(cfg.min_p); - llama->set_penalty_repeat(cfg.penalty_repeat); - llama->set_penalty_last_n(cfg.penalty_last_n); - llama->set_log_level(cfg.log_level); +void TuiState::redraw_all() { + redraw_header(); + redraw_chat(); + redraw_input(); + notcurses_render(nc); +} + +void TuiState::tick_spinner() { + ++spinner_frame; + redraw_header(); + notcurses_render(nc); +} + +void TuiState::set_thinking(bool on) { + thinking = on; + if (!on) spinner_frame = 0; + redraw_header(); + notcurses_render(nc); } // -// Shows a modal loading popup while the model loads. +// TuiState content helpers // -bool AgentState::setup_model(const NitroConfig &cfg, TuiState &tui) { - if (cfg.model_path.empty()) { - tui.append_line("[sys] No model loaded. Use /model to load a GGUF."); - tui.redraw_all(); - return false; - } - // Show a modal popup so the user knows loading is in progress. - std::string model_name = fs::path(cfg.model_path).filename().string(); - tui.show_modal_popup("Loading " + model_name); - // Destroy the iterator first β€” it holds references into the llama context. - // Freeing llama while iter is still alive causes use-after-free / load failure. - iter.reset(); - model_loaded = false; - llama = std::make_unique(); - - apply_generation_params(cfg); - if (!llama->load_model(cfg.model_path, cfg.n_ctx, cfg.n_batch, - cfg.n_gpu_layers, cfg.log_level)) { - tui.dismiss_modal_popup(); - tui.append_line(std::string("[err] ") + llama->last_error()); - tui.redraw_all(); - return false; +void TuiState::append_line(const std::string &line) { + std::lock_guard lk(lines_mutex); + int w = std::max(1, term_cols - 1); + if ((int)line.size() <= w) { + chat_lines.push_back(line); + } else { + for (int off = 0; off < (int)line.size(); off += w) { + chat_lines.push_back(line.substr(off, w)); + } } - tui.dismiss_modal_popup(); - model_loaded = true; - tui.current_model = model_name; - tui.append_line("[sys] Model ready: " + tui.current_model); - LlamaMemoryInfo mem = llama->memory_info(); - tui.append_line("[sys] " + mem.advice); - tui.kv_used = mem.kv_used; - tui.kv_total = mem.kv_total; - tui.vram_used = mem.vram_used; - tui.vram_total = mem.vram_total; - tui.redraw_all(); - return true; } -bool AgentState::setup_embed(const std::string &path, TuiState &tui) { - tui.show_modal_popup("Loading embedding model: " + fs::path(path).filename().string()); - tui.redraw_all(); - embed_llama = std::make_unique(); - if (!embed_llama->load_embedding_model(path)) { - tui.dismiss_modal_popup(); - tui.append_line(std::string("[err] ") + embed_llama->last_error()); - tui.redraw_all(); - embed_llama.reset(); - return false; +void TuiState::append_token(const std::string &token) { + token_acc += token; + for (;;) { + auto pos = token_acc.find('\n'); + if (pos == std::string::npos) { + break; + } + append_line(token_acc.substr(0, pos)); + token_acc = token_acc.substr(pos + 1); } - tui.dismiss_modal_popup(); - rag_db = std::make_unique(); - rag_session = std::make_unique(); - tui.append_line("[sys] Embedding model ready."); - tui.redraw_all(); - return true; + redraw_chat(); + notcurses_render(nc); } -void AgentState::reset_conversation(const std::string &sysprompt, TuiState &tui) { - system_prompt = sysprompt; - llama->reset(); - apply_generation_params(NitroConfig{}); - iter = std::make_unique(); - if (!llama->add_message(*iter, "system", system_prompt)) { - tui.append_line(std::string("[err] System prompt injection: ") + llama->last_error()); - tui.redraw_all(); +void TuiState::flush_token_acc() { + if (!token_acc.empty()) { + append_line(token_acc); + token_acc.clear(); + redraw_chat(); + notcurses_render(nc); } } -float AgentState::tokens_per_sec() const { - if (!iter) return 0.0f; - auto now = std::chrono::high_resolution_clock::now(); - double elapsed = std::chrono::duration(now - iter->_t_start).count(); - if (elapsed <= 0.0 || iter->_tokens_generated <= 0) return 0.0f; - return (float)(iter->_tokens_generated / elapsed); -} +// +// Creates a centred floating plane with a border and a status message. +// The popup sits above all other planes and blocks until explicitly dismissed. +// +void TuiState::show_modal_popup(const std::string &message) { + // Dismiss any previous popup first. + dismiss_modal_popup(); -std::string AgentState::memory_info_text() { - if (!model_loaded) return "No model loaded."; - LlamaMemoryInfo m = llama->memory_info(); - std::ostringstream oss; - oss << "KV cache : " << m.kv_used << " / " << m.kv_total - << " (" << m.kv_percent << "%)\n"; - if (m.vram_total > 0) { - oss << "VRAM : " << (m.vram_used >> 20) << " MB / " - << (m.vram_total >> 20) << " MB (" << m.vram_percent << "%)\n"; - } - oss << "GPU layers: " << m.n_layers_gpu << " / " << m.n_layers_total << "\n"; - oss << "CPU layers: " << m.n_layers_cpu << "\n"; - oss << "Advice : " << m.advice << "\n"; - return oss.str(); -} + // Clamp popup size to terminal. + int popup_w = std::min((int)message.size() + 8, term_cols - 4); + popup_w = std::max(popup_w, 20); + int popup_h = 5; + int py = std::max(0, (term_rows - popup_h) / 2); + int px = std::max(0, (term_cols - popup_w) / 2); -std::string AgentState::rag_tool(const NitroConfig &cfg, const std::string &agent_query) { - std::string result; - if (embed_llama && rag_db && rag_session) { - result = embed_llama->rag_retrieve(*rag_db, agent_query, cfg.rag_top_k, *rag_session); - if (result.empty()) { - result = "RAG: no context found"; - } - } else { - result = "RAG: not enabled"; + ncplane_options opts{}; + opts.y = py; opts.x = px; + opts.rows = (unsigned)popup_h; + opts.cols = (unsigned)popup_w; + modal_plane = ncplane_create(stdpl, &opts); + if (!modal_plane) return; + + // Background: deep navy. + static constexpr uint32_t PBG_R = 20, PBG_G = 28, PBG_B = 50; + ncplane_set_base(modal_plane, " ", 0, + NCCHANNELS_INITIALIZER(PBG_R, PBG_G, PBG_B, PBG_R, PBG_G, PBG_B)); + ncplane_erase(modal_plane); + + // Border β€” bright cyan. + uint64_t border_ch = NCCHANNELS_INITIALIZER(80, 220, 255, PBG_R, PBG_G, PBG_B); + ncplane_set_channels(modal_plane, border_ch); + + // Draw corners and edges manually so we don't require nccell border helpers. + // Top row + ncplane_putstr_yx(modal_plane, 0, 0, "β•”"); + for (int c = 1; c < popup_w - 1; ++c) + ncplane_putstr_yx(modal_plane, 0, c, "═"); + ncplane_putstr_yx(modal_plane, 0, popup_w - 1, "β•—"); + // Middle rows + for (int r = 1; r < popup_h - 1; ++r) { + ncplane_putstr_yx(modal_plane, r, 0, "β•‘"); + ncplane_putstr_yx(modal_plane, r, popup_w - 1, "β•‘"); } - return result; + // Bottom row + ncplane_putstr_yx(modal_plane, popup_h - 1, 0, "β•š"); + for (int c = 1; c < popup_w - 1; ++c) + ncplane_putstr_yx(modal_plane, popup_h - 1, c, "═"); + ncplane_putstr_yx(modal_plane, popup_h - 1, popup_w - 1, "╝"); + + // Title bar. + uint64_t title_ch = NCCHANNELS_INITIALIZER(255, 220, 80, PBG_R, PBG_G, PBG_B); + ncplane_set_channels(modal_plane, title_ch); + ncplane_putstr_yx(modal_plane, 1, 2, "⏳ Loading…"); + + // Message. + uint64_t msg_ch = NCCHANNELS_INITIALIZER(200, 200, 200, PBG_R, PBG_G, PBG_B); + ncplane_set_channels(modal_plane, msg_ch); + // Truncate message to fit inside border. + int max_msg = popup_w - 4; + std::string display = message.size() > (size_t)max_msg + ? message.substr(0, max_msg) + : message; + ncplane_putstr_yx(modal_plane, 2, 2, display.c_str()); + + notcurses_render(nc); } -bool AgentState::rag_load_index(const std::string &path, TuiState &tui) { - if (!embed_llama || !rag_db) { - tui.append_line("[err] Load an embedding model first: /embed "); - tui.redraw_all(); - return false; - } +void TuiState::show_help() { + append_line("[sys] Commands:"); + append_line("[sys] /model [path] load a GGUF model (picker if no path)"); + append_line("[sys] /embed [path] load an embedding model (picker if no path)"); + append_line("[sys] /rag [path] index file or directory (picker if no path)"); + append_line("[sys] /memory KV / VRAM / layer stats"); + append_line("[sys] /clear reset conversation"); + append_line("[sys] /settings show current settings"); + append_line("[sys] /set change a setting live"); + append_line("[sys] /help this message"); + append_line("[sys] exit / quit exit Nitro"); + append_line("[sys] Settable keys (via /set):"); + append_line("[sys] temperature top_p top_k min_p penalty_repeat"); + append_line("[sys] n_max_tokens penalty_last_n rag_top_k n_gpu_layers"); + append_line("[sys] run_allowed (comma-separated list, e.g. python3,make)"); + redraw_all(); +} - if (!rag_db->load(path)) { - tui.append_line("[sys] failed to load"); - tui.redraw_all(); +void TuiState::dismiss_modal_popup() { + if (modal_plane) { + ncplane_destroy(modal_plane); + modal_plane = nullptr; + notcurses_render(nc); } - - return true; } -bool AgentState::rag_index(const std::string &path, const NitroConfig &cfg, TuiState &tui) { - if (!embed_llama || !rag_db) { - tui.append_line("[err] Load an embedding model first: /embed "); - tui.redraw_all(); - return false; +// +// ─── TuiState::file_picker ──────────────────────────────────────────────── +// Interactive directory/file browser popup. +// Keyboard: ↑/↓ navigate, Enter select/descend, Backspace go up, +// 's' select current dir for indexing, Esc cancel. +// Returns the chosen path or "" on cancel. +// ─── TuiState::file_picker ──────────────────────────────────────────────── +// Unified interactive directory/file browser used by /rag, /model, /embed. +// title_hint appears in the popup header (e.g. "RAG Folder", "Model File"). +// +// Keyboard: +// ↑/↓ navigate list +// Enter descend into directory, or select a file +// Backspace go up one directory +// s select the current directory itself (useful for /rag) +// Esc cancel β†’ returns "" +// +// Returns the chosen path, or "" on cancel. +// +std::string TuiState::file_picker(const std::string &start_dir, + const std::string &title_hint) { + std::string current_dir = start_dir; + { + std::error_code ec; + auto canon = fs::canonical(start_dir, ec); + if (!ec) current_dir = canon.string(); } - - auto index_one = [&](const std::string &filepath) { - tui.append_line("[sys] indexing: " + filepath); - tui.redraw_all(); - if (!embed_llama->rag_index(*rag_db, filepath)) { - tui.append_line(std::string("[err] rag_load: ") + embed_llama->last_error()); - tui.redraw_all(); + auto load_entries = [](const std::string &dir, + std::vector &entries) { + entries.clear(); + std::error_code ec; + if (fs::path(dir).has_parent_path() && + fs::path(dir) != fs::path(dir).root_path()) + entries.push_back(".."); + std::vector dirs, files; + for (const auto &e : fs::directory_iterator(dir, ec)) { + if (ec) break; + std::string name = e.path().filename().string(); + if (name.empty() || name[0] == '.') continue; + if (e.is_directory()) dirs.push_back(name); + else files.push_back(name); } + std::sort(dirs.begin(), dirs.end()); + std::sort(files.begin(), files.end()); + for (auto &d : dirs) entries.push_back(d + "/"); + for (auto &f : files) entries.push_back(f); }; - // must be set before indexing - rag_db->embed_dim = embed_llama->get_embed_dim(); - - fs::path rp(path); - std::error_code ec; - if (fs::is_directory(rp, ec)) { - for (const auto &entry : fs::recursive_directory_iterator(rp, ec)) { - if (entry.is_regular_file()) { - index_one(entry.path().string()); - } - } - } else { - index_one(path); - } - - std::string save_path = join_path(cfg.sandbox, "rag-index.bin"); - tui.append_line("[sys] saving index: " + save_path); - tui.redraw_all(); - rag_db->save(save_path); + std::vector entries; + int selected = 0; + int scroll = 0; - return true; -} + // Popup dimensions. + static constexpr int PW = 60; + static constexpr int PH = 20; + int py = std::max(0, (term_rows - PH) / 2); + int px = std::max(0, (term_cols - PW) / 2); -// -// Tool dispatch -// -std::string AgentState::process_tool(const std::string &cmd, const NitroConfig &cfg, TuiState &tui) { - const std::string &sandbox = cfg.sandbox; - const std::vector &run_allowed = cfg.run_allowed; + ncplane_options opts{}; + opts.y = py; opts.x = px; + opts.rows = (unsigned)PH; opts.cols = (unsigned)PW; + struct ncplane *picker = ncplane_create(stdpl, &opts); + if (!picker) return ""; - std::string op, arg1, arg2; - auto sp1 = cmd.find(' '); - if (sp1 == std::string::npos) { - op = trim(cmd); - } else { - op = trim(cmd.substr(0, sp1)); - std::string rest = cmd.substr(sp1 + 1); - rest.erase(0, rest.find_first_not_of(" \t")); - auto sp2 = rest.find(' '); - if (sp2 == std::string::npos) { - arg1 = rest; - } else { - arg1 = rest.substr(0, sp2); - arg2 = rest.substr(sp2 + 1); - } + static constexpr uint32_t PBG_R = 18, PBG_G = 24, PBG_B = 40; + ncplane_set_base(picker, " ", 0, NCCHANNELS_INITIALIZER(PBG_R, PBG_G, PBG_B, PBG_R, PBG_G, PBG_B)); + // Build a compact hint line appropriate to the operation. + // /rag adds 's=select dir'; /model and /embed only need file selection. + std::string hint_line = "↑↓ navigate Enter open/select Esc cancel"; + if (title_hint.find("RAG") != std::string::npos || + title_hint.find("Folder") != std::string::npos) { + hint_line = "↑↓ navigate Enter open s=select dir Esc cancel"; } - - auto resolve = [&](const std::string &p) -> std::string { - if (p.empty() || p == ".") { - return sandbox; - } - if (p.substr(0, 2) == "./") { - return join_path(sandbox, p.substr(2)); + auto draw_picker = [&]() { + ncplane_erase(picker); + uint64_t border_ch = NCCHANNELS_INITIALIZER(100, 180, 255, PBG_R, PBG_G, PBG_B); + ncplane_set_channels(picker, border_ch); + ncplane_putstr_yx(picker, 0, 0, "β•”"); + for (int c = 1; c < PW - 1; ++c) ncplane_putstr_yx(picker, 0, c, "═"); + ncplane_putstr_yx(picker, 0, PW - 1, "β•—"); + for (int r = 1; r < PH - 1; ++r) { + ncplane_putstr_yx(picker, r, 0, "β•‘"); + ncplane_putstr_yx(picker, r, PW - 1, "β•‘"); } - if (p[0] == '/') { - return p; + ncplane_putstr_yx(picker, PH - 1, 0, "β•š"); + for (int c = 1; c < PW - 1; ++c) ncplane_putstr_yx(picker, PH - 1, c, "═"); + ncplane_putstr_yx(picker, PH - 1, PW - 1, "╝"); + + // Title + ncplane_set_channels(picker, NCCHANNELS_INITIALIZER(255, 220, 80, PBG_R, PBG_G, PBG_B)); + std::string title_str = " πŸ“‚ " + title_hint + " Picker "; + if ((int)title_str.size() > PW - 4) title_str = title_str.substr(0, PW - 4); + ncplane_putstr_yx(picker, 0, 2, title_str.c_str()); + // Current path (truncated). + std::string path_display = current_dir; + if ((int)path_display.size() > PW - 4) + path_display = "…" + path_display.substr(path_display.size() - (PW - 5)); + ncplane_set_channels(picker, NCCHANNELS_INITIALIZER(160, 200, 240, PBG_R, PBG_G, PBG_B)); + ncplane_putstr_yx(picker, 1, 2, path_display.c_str()); + // Hint line (bottom interior row). + ncplane_set_channels(picker, NCCHANNELS_INITIALIZER(120, 120, 160, PBG_R, PBG_G, PBG_B)); + std::string hint_trunc = hint_line; + if ((int)hint_trunc.size() > PW - 4) hint_trunc = hint_trunc.substr(0, PW - 4); + ncplane_putstr_yx(picker, PH - 2, 2, hint_trunc.c_str()); + // Entry list. + int list_rows = PH - 5; + if (selected < scroll) scroll = selected; + if (selected >= scroll + list_rows) scroll = selected - list_rows + 1; + for (int i = 0; i < list_rows; ++i) { + int idx = scroll + i; + if (idx >= (int)entries.size()) break; + bool is_selected = (idx == selected); + bool is_dir = !entries[idx].empty() && entries[idx].back() == '/'; + uint32_t fr, fg, fb; + if (is_selected) { fr = 20; fg = 20; fb = 20; } + else if (is_dir) { fr = 120; fg = 200; fb = 255; } + else { fr = 200; fg = 200; fb = 200; } + uint32_t br = is_selected ? 100 : PBG_R; + uint32_t bg = is_selected ? 180 : PBG_G; + uint32_t bb = is_selected ? 255 : PBG_B; + ncplane_set_channels(picker, NCCHANNELS_INITIALIZER(fr, fg, fb, br, bg, bb)); + std::string label = (is_selected ? " β–Ά " : " ") + entries[idx]; + if ((int)label.size() > PW - 2) label = label.substr(0, PW - 2); + while ((int)label.size() < PW - 2) label += ' '; + ncplane_putstr_yx(picker, 2 + i, 1, label.c_str()); } - return join_path(sandbox, disclose(disclose(p, '<', '>'), '[', ']')); + notcurses_render(nc); }; - tui.append_line("[tool] β†’ " + op); - tui.redraw_all(); - - if (op == "TOOL:DATE") { - char buf[32]; time_t t = time(nullptr); - strftime(buf, sizeof(buf), "%Y-%m-%d", localtime(&t)); - return buf; - } - if (op == "TOOL:TIME") { - char buf[32]; time_t t = time(nullptr); - strftime(buf, sizeof(buf), "%H:%M:%S", localtime(&t)); - return buf; - } - if (op == "TOOL:RND") { - return std::to_string((double)rand() / RAND_MAX); - } - if (op == "TOOL:RAG") { - return rag_tool(cfg, arg1); - } - if (op == "TOOL:LIST") { - std::string dir = resolve(arg1); - if (!path_in_sandbox(sandbox, dir)) return "ERROR: path outside sandbox"; - return list_dir(dir); - } - if (op == "TOOL:EXISTS") { - std::string p = resolve(arg1); - if (!path_in_sandbox(sandbox, p)) return "NO"; - return fs::exists(p) ? "YES" : "NO"; - } - if (op == "TOOL:READ") { - std::string p = resolve(arg1); - if (!path_in_sandbox(sandbox, p)) return "ERROR: path outside sandbox"; - return read_file(p); - } - if (op == "TOOL:WRITE") { - std::string p = resolve(arg1); - if (!path_in_sandbox(sandbox, p)) { - return "ERROR: path outside sandbox"; - } - if (!tui.confirm_dialog(std::format("Allow model to write {}?", p))) { - return "ERROR: action prevented by user"; + std::string result; + load_entries(current_dir, entries); + draw_picker(); + + for (;;) { + ncinput ni{}; + notcurses_get_blocking(nc, &ni); + if (ni.id == NCKEY_ESC) { + break; // cancelled } - std::string content = disclose(strip_code_fences(arg1, arg2), '`', '`'); - return write_file(p, content) ? "OK: written to " + arg1 : "ERROR: write failed for " + arg1; - } - if (op == "TOOL:CURL") { - return tool_curl(arg1); - } - if (op == "TOOL:INTROSPECT") { - return introspect(cfg); - } - if (op == "TOOL:RUN") { - std::string prog = resolve(arg1); - if (!path_in_sandbox(sandbox, prog)) { - return "ERROR: path outside sandbox"; + if (ni.id == NCKEY_UP) { + if (selected > 0) --selected; + draw_picker(); + continue; } - if (!run_allowed.empty()) { - std::string basename = fs::path(prog).filename().string(); - bool permitted = std::any_of(run_allowed.begin(), run_allowed.end(), - [&](const std::string &a){ return a == basename; }); - if (!permitted) { - return "ERROR: '" + basename + "' is not in the TOOL:RUN allowlist. " - "Use /set run_allowed to permit it."; - } - } else if (!tui.confirm_dialog(std::format("Allow {} to run?", prog))) { - return "ERROR: prevented by user"; + if (ni.id == NCKEY_DOWN) { + if (selected + 1 < (int)entries.size()) ++selected; + draw_picker(); + continue; } - std::string command = prog + " " + arg2 + " 2>&1"; - FILE *fp = popen(command.c_str(), "r"); - if (!fp) { - return "ERROR: popen failed"; + // 's' β€” select the current directory (useful for /rag, ignored for file pickers). + if (ni.id == 's' || ni.id == 'S') { + // Select current directory for RAG indexing. + result = current_dir; + break; } - std::string out; - char buf[256]; - while (fgets(buf, sizeof(buf), fp)) { - out += buf; + if (ni.id == NCKEY_BACKSPACE || ni.id == 127) { + // Go up one level. + fs::path p(current_dir); + if (p.has_parent_path() && p != p.root_path()) { + current_dir = p.parent_path().string(); + load_entries(current_dir, entries); + selected = 0; scroll = 0; + draw_picker(); + } + continue; } - pclose(fp); - if (out.size() > 4096) { - out = out.substr(0, 4096) + "\n…(truncated)"; + if (ni.id == NCKEY_ENTER || ni.id == '\r' || ni.id == '\n') { + if (entries.empty()) continue; + const std::string &entry = entries[selected]; + if (entry == "..") { + fs::path p(current_dir); + if (p.has_parent_path() && p != p.root_path()) { + current_dir = p.parent_path().string(); + load_entries(current_dir, entries); + selected = 0; scroll = 0; + draw_picker(); + } + } else if (!entry.empty() && entry.back() == '/') { + // Descend into directory. + current_dir = current_dir + "/" + entry.substr(0, entry.size() - 1); + { + std::error_code ec; + auto canon = fs::canonical(current_dir, ec); + if (!ec) current_dir = canon.string(); + } + load_entries(current_dir, entries); + selected = 0; scroll = 0; + draw_picker(); + } else { + // Select a specific file. + // Select the highlighted file. + result = current_dir + "/" + entry; + break; + } + continue; } - return out; } - return "ERROR: unknown tool: [" + op + "]"; + ncplane_destroy(picker); + notcurses_render(nc); + return result; } // -// Agent turn +// ─── TuiState::confirm_dialog ───────────────────────────────────────────── // -bool AgentState::run_turn(const std::string &user_message, const NitroConfig &cfg, TuiState &tui) { - if (!model_loaded) { - tui.append_line("[err] No model loaded. Use /model "); - tui.redraw_all(); - return false; - } - std::string effective_message = user_message; - if (embed_llama && rag_db && rag_session) { - std::string context = embed_llama->rag_retrieve(*rag_db, user_message, cfg.rag_top_k, *rag_session); - if (!context.empty()) { - log_write("RAG: %s", context.c_str()); - effective_message = "Context:\n" + context + "\n\nUser: " + user_message; - } else { - log_write("RAG: no context found [%s]", embed_llama->last_error()); - } - } - if (!iter) { - tui.append_line("[err] Conversation not initialised (call /clear to reset)"); - tui.redraw_all(); - return false; - } - if (!llama->add_message(*iter, "user", effective_message)) { - tui.append_line(std::string("[err] add_message: ") + llama->last_error()); - tui.redraw_all(); - return false; +bool TuiState::confirm_dialog(const std::string &prompt) { + ncplane_erase(inputpl); + ncplane_set_channels(inputpl, inp_ch(255, 200, 80)); + std::string msg = " " + prompt + " [y/n] ❯ "; + ncplane_putstr_yx(inputpl, 1, 0, msg.c_str()); + notcurses_render(nc); + std::string answer; + for (;;) { + ncinput ni{}; + notcurses_get_blocking(nc, &ni); + if (ni.id == NCKEY_ENTER || ni.id == '\r' || ni.id == '\n') break; + if (ni.id == NCKEY_BACKSPACE && !answer.empty()) { answer.pop_back(); } + else if (ni.id >= 32 && ni.id < 127) { answer += (char)ni.id; } + ncplane_erase(inputpl); + ncplane_set_channels(inputpl, inp_ch(255, 200, 80)); + ncplane_putstr_yx(inputpl, 1, 0, (msg + answer).c_str()); + notcurses_render(nc); } - tui.append_line("Nitro: "); + std::string lo = answer; + std::transform(lo.begin(), lo.end(), lo.begin(), ::tolower); + redraw_input(); + notcurses_render(nc); + return (lo == "y" || lo == "yes" || lo == "sure" || lo == "k"); +} - // in_think starts false β€” models that don't use blocks emit - // visible text immediately. The spinner activates only while thinking. - enum {t_init, t_think, t_thunk} think_mode = t_init; - tui.set_thinking(false); - std::string buffer; +// +// Integrates InputHistory: Up/Down arrows navigate the history stack. +// On submit the entry is pushed to history, and nav is reset. +// +std::string TuiState::readline_blocking() { + input_buf.clear(); + cursor_pos = 0; + history.reset_nav(); + redraw_input(); + notcurses_render(nc); - auto invoke_tool = [&](const std::string &tool, const std::string_view template_str) -> void { - std::string result = process_tool(tool, cfg, tui); - std::string content = std::vformat(template_str, std::make_format_args(result)); - log_write("tool: [%s] result: [%s]", tool.c_str(), result.c_str()); - if (!llama->add_message(*iter, "tool_result", content)) { - tui.append_line(std::string("[err] tool result inject: ") + llama->last_error()); - tui.redraw_all(); - } - if (!iter->_has_next) { - tui.append_line(std::string("[err] failed to evoke tool response: ") + llama->last_error()); - tui.redraw_all(); + // Temporary saved draft so Down from history restores the user's current text. + std::string draft; + + for (;;) { + ncinput ni{}; + notcurses_get_blocking(nc, &ni); + + if (ni.id == NCKEY_ENTER || ni.id == '\r' || ni.id == '\n') { + std::string result = input_buf; + if (!result.empty()) { + history.push(result); + } + input_buf.clear(); + cursor_pos = 0; + redraw_input(); + notcurses_render(nc); + return result; } - }; - auto start_think = [&](const std::string &tag) { - if (think_mode == t_init) { - auto pos = buffer.find(tag); - if (pos != std::string::npos) { - think_mode = t_think; - tui.set_thinking(true); - // display prededing text - buffer = buffer.substr(0, pos); + if (ni.id == NCKEY_UP) { + // Entering history from a fresh prompt: save current text as draft. + std::string hist_entry; + if (history.up(hist_entry)) { + if (input_buf.size() > 0 && hist_entry != input_buf) { + // Only save draft when we first leave the bottom of history. + // (history.reset_nav was called on entry so the first Up call + // always comes from the "new input" position.) + draft = input_buf; + } + input_buf = hist_entry; + cursor_pos = input_buf.size(); } + redraw_input(); + notcurses_render(nc); + continue; } - }; - auto end_think = [&](const std::string &tag) { - if (think_mode == t_think) { - auto pos = buffer.find(tag); - if (pos != std::string::npos) { - think_mode = t_thunk; - tui.set_thinking(false); - // display remaining text - buffer = buffer.substr(pos + tag.length()); + if (ni.id == NCKEY_DOWN) { + std::string hist_entry; + bool got = history.down(hist_entry); + if (got) { + input_buf = hist_entry; + cursor_pos = input_buf.size(); + } else { + // Past the newest entry β†’ restore draft. + input_buf = draft; + cursor_pos = input_buf.size(); + draft.clear(); } + redraw_input(); + notcurses_render(nc); + continue; } - }; - while (iter->_has_next) { - ncinput ni{}; - notcurses_get_nblock(tui.nc, &ni); - if (ni.id == NCKEY_ESC) { - tui.set_thinking(false); - tui.append_line("[err] Generation cancelled by user (Escape)"); - tui.redraw_all(); - return false; + // Scroll the chat pane β€” not the input history. + if (ni.id == NCKEY_PGUP) { + scroll_offset += std::max(1, term_rows - 4); + redraw_chat(); + notcurses_render(nc); + continue; + } + if (ni.id == NCKEY_PGDOWN) { + scroll_offset = std::max(0, scroll_offset - std::max(1, term_rows - 4)); + redraw_chat(); + notcurses_render(nc); + continue; } - std::string tok = llama->next(*iter); - buffer += tok; - if (think_mode == t_init) { - start_think(""); - start_think("<|think|>"); - start_think(""); - start_think("<|channel>thought"); + if (ni.id == NCKEY_SCROLL_UP && scroll_offset < term_rows + 10) { + scroll_offset += 1; + redraw_chat(); + notcurses_render(nc); + continue; } - if (think_mode == t_think) { - tui.tick_spinner(); - end_think(""); - end_think(""); - end_think(""); - end_think(""); + if (ni.id == NCKEY_SCROLL_DOWN && scroll_offset > 0) { + scroll_offset -= 1; + redraw_chat(); + notcurses_render(nc); + continue; } - if (think_mode == t_thunk) { - auto tool_start = buffer.find("TOOL:"); - if (tool_start == 0) { - // fetch all remaining tokens - invoke_tool(trim(buffer + llama->all(*iter)), "TOOL_RESULT: {}"); - buffer.clear(); - think_mode = t_init; - continue; - } - // see https://ai.google.dev/gemma/docs/core/prompt-formatting-gemma4 - tool_start = buffer.find("<|tool_call>call:"); - if (tool_start != std::string::npos) { - buffer += llama->all(*iter); - auto pos = buffer.find_last_not_of("}"); - if (pos != std::string::npos) { - buffer = buffer.substr(0, pos); - } - pos = buffer.find_first_not_of("{"); - if (pos != std::string::npos) { - buffer = buffer.substr(0, pos) + buffer.substr(pos + 1); - } - invoke_tool(trim(buffer), "<|tool_response>{}"); - buffer.clear(); - think_mode = t_init; - continue; - } - auto pos = buffer.find('\n'); - if (pos != std::string::npos) { - tui.append_token(buffer.substr(0, pos + 1)); - buffer = buffer.substr(pos + 1); + if (ni.id == NCKEY_F01) { + show_help(); + continue; + } + if (ni.id == NCKEY_F02) { + mouse_mode = !mouse_mode; + if (mouse_mode) { + notcurses_mice_enable(nc, NCMICE_BUTTON_EVENT); + } else { + notcurses_mice_disable(nc); } + continue; + } + if (ni.id == NCKEY_BACKSPACE || ni.id == 127) { + if (cursor_pos > 0) { input_buf.erase(cursor_pos - 1, 1); --cursor_pos; } + } else if (ni.id == NCKEY_LEFT) { + if (cursor_pos > 0) --cursor_pos; + } else if (ni.id == NCKEY_RIGHT) { + if (cursor_pos < input_buf.size()) ++cursor_pos; + } else if (ni.id == NCKEY_HOME) { + cursor_pos = 0; + } else if (ni.id == NCKEY_END) { + cursor_pos = input_buf.size(); + } else if (ni.id == NCKEY_DEL) { + if (cursor_pos < input_buf.size()) input_buf.erase(cursor_pos, 1); + } else if (ni.id >= 32 && ni.id < 0xD800) { + // Any printable character β€” entering new text clears the nav draft + // so that Down won't resurrect a stale saved buffer. + draft.clear(); + history.reset_nav(); + input_buf.insert(cursor_pos, 1, (char)ni.id); + ++cursor_pos; } + + redraw_input(); + notcurses_render(nc); } +} - if (!buffer.empty()) { - tui.append_token(buffer + "\n"); +void AgentState::apply_generation_params(const NitroConfig &cfg) { + llama->add_stop("<|turn|>"); + llama->add_stop("<|im_end|>"); + llama->set_max_tokens(cfg.n_max_tokens); + llama->set_temperature(cfg.temperature); + llama->set_top_k(cfg.top_k); + llama->set_top_p(cfg.top_p); + llama->set_min_p(cfg.min_p); + llama->set_penalty_repeat(cfg.penalty_repeat); + llama->set_penalty_last_n(cfg.penalty_last_n); + llama->set_log_level(cfg.log_level); +} + +// +// Shows a modal loading popup while the model loads. +// +bool AgentState::setup_model(const NitroConfig &cfg, TuiState &tui) { + if (cfg.model_path.empty()) { + tui.append_line("[sys] No model loaded. Use /model to load a GGUF."); + tui.redraw_all(); + return false; } + // Show a modal popup so the user knows loading is in progress. + std::string model_name = fs::path(cfg.model_path).filename().string(); + tui.show_modal_popup("Loading " + model_name); + // Destroy the iterator first β€” it holds references into the llama context. + // Freeing llama while iter is still alive causes use-after-free / load failure. + iter.reset(); + model_loaded = false; + llama = std::make_unique(); - tui.flush_token_acc(); - tui.set_thinking(false); - tui.tokens_per_sec = tokens_per_sec(); + apply_generation_params(cfg); + if (!llama->load_model(cfg.model_path, cfg.n_ctx, cfg.n_batch, + cfg.n_gpu_layers, cfg.log_level)) { + tui.dismiss_modal_popup(); + tui.append_line(std::string("[err] ") + llama->last_error()); + tui.redraw_all(); + return false; + } + tui.dismiss_modal_popup(); + model_loaded = true; + tui.current_model = model_name; + tui.append_line("[sys] Model ready: " + tui.current_model); LlamaMemoryInfo mem = llama->memory_info(); - tui.kv_used = mem.kv_used; - tui.kv_total = mem.kv_total; + tui.append_line("[sys] " + mem.advice); + tui.kv_used = mem.kv_used; + tui.kv_total = mem.kv_total; tui.vram_used = mem.vram_used; tui.vram_total = mem.vram_total; - char stat[128]; - std::snprintf(stat, sizeof(stat), "[sys] %.1f tok/s (%d tokens) KV %.1f%%", - (double)tui.tokens_per_sec, - iter->_tokens_generated, - (double)mem.kv_percent); - tui.append_line(stat); tui.redraw_all(); return true; } -// -// File-system helpers -// -static std::string join_path(const std::string &a, const std::string &b) { - if (b.empty()) return a; - if (b[0] == '/') return b; - std::string pa = a; - if (!pa.empty() && pa.back() == '/') pa.pop_back(); - std::string pb = (b.front() == '/') ? b.substr(1) : b; - return pa + "/" + pb; -} - -static bool path_in_sandbox(const std::string &sandbox, const std::string &path) { - std::error_code ec; - auto base = fs::canonical(sandbox, ec); if (ec) return false; - auto target = fs::weakly_canonical(path, ec); - std::string bstr = base.string() + "/"; - std::string tstr = target.string(); - return tstr == base.string() || tstr.compare(0, bstr.size(), bstr) == 0; +bool AgentState::setup_embed(const std::string &path, TuiState &tui) { + tui.show_modal_popup("Loading embedding model: " + fs::path(path).filename().string()); + tui.redraw_all(); + embed_llama = std::make_unique(); + if (!embed_llama->load_embedding_model(path)) { + tui.dismiss_modal_popup(); + tui.append_line(std::string("[err] ") + embed_llama->last_error()); + tui.redraw_all(); + embed_llama.reset(); + return false; + } + tui.dismiss_modal_popup(); + rag_db = std::make_unique(); + rag_session = std::make_unique(); + tui.append_line("[sys] Embedding model ready."); + tui.redraw_all(); + return true; } -static std::string read_file(const std::string &path) { - std::ifstream f(path, std::ios::binary); - if (!f) { - return "ERROR: cannot open [" + path + "]"; +void AgentState::reset_conversation(const std::string &sysprompt, TuiState &tui) { + system_prompt = sysprompt; + llama->reset(); + apply_generation_params(NitroConfig{}); + iter = std::make_unique(); + if (!llama->add_message(*iter, "system", system_prompt)) { + tui.append_line(std::string("[err] System prompt injection: ") + llama->last_error()); + tui.redraw_all(); } - std::ostringstream oss; oss << f.rdbuf(); - return oss.str(); } -static bool write_file(const std::string &path, const std::string &data) { - fs::path p(path); - if (p.has_parent_path()) { - std::error_code ec; - fs::create_directories(p.parent_path(), ec); - } - std::ofstream f(path, std::ios::binary | std::ios::trunc); - if (!f) return false; - f.write(data.data(), (std::streamsize)data.size()); - return f.good(); +float AgentState::tokens_per_sec() const { + if (!iter) return 0.0f; + auto now = std::chrono::high_resolution_clock::now(); + double elapsed = std::chrono::duration(now - iter->_t_start).count(); + if (elapsed <= 0.0 || iter->_tokens_generated <= 0) return 0.0f; + return (float)(iter->_tokens_generated / elapsed); } -static std::string list_dir(const std::string &path) { +std::string AgentState::memory_info_text() { + if (!model_loaded) return "No model loaded."; + LlamaMemoryInfo m = llama->memory_info(); std::ostringstream oss; - std::error_code ec; - for (const auto &e : fs::directory_iterator(path, ec)) { - if (ec) break; - std::string name = e.path().filename().string(); - if (name.empty() || name[0] == '.') continue; - oss << (e.is_directory() ? "[" + name + "]" : name) << "\n"; + oss << "KV cache : " << m.kv_used << " / " << m.kv_total + << " (" << m.kv_percent << "%)\n"; + if (m.vram_total > 0) { + oss << "VRAM : " << (m.vram_used >> 20) << " MB / " + << (m.vram_total >> 20) << " MB (" << m.vram_percent << "%)\n"; } + oss << "GPU layers: " << m.n_layers_gpu << " / " << m.n_layers_total << "\n"; + oss << "CPU layers: " << m.n_layers_cpu << "\n"; + oss << "Advice : " << m.advice << "\n"; return oss.str(); } -static const std::vector CODE_EXTENSIONS = { - ".py",".c",".cpp",".h",".bas",".java",".html",".js",".ts", - ".json",".yaml",".toml",".sh",".go",".rs",".jsx",".tsx" -}; - -static std::string strip_code_fences(const std::string &filename, - const std::string &src) { - auto ext = fs::path(filename).extension().string(); - bool is_code = std::any_of(CODE_EXTENSIONS.begin(), CODE_EXTENSIONS.end(), - [&](const std::string &e){ return ext == e; }); - if (!is_code) return src; - auto pos = src.find("```"); - if (pos == std::string::npos) return src; - auto nl = src.find('\n', pos + 3); - if (nl == std::string::npos) return src; - std::string inner = src.substr(nl + 1); - auto end = inner.rfind("```"); - if (end != std::string::npos) inner = inner.substr(0, end); - return inner; +std::string AgentState::rag_tool(const NitroConfig &cfg, const std::string &agent_query) { + std::string result; + if (embed_llama && rag_db && rag_session) { + result = embed_llama->rag_retrieve(*rag_db, agent_query, cfg.rag_top_k, *rag_session); + if (result.empty()) { + result = "RAG: no context found"; + } + } else { + result = "RAG: not enabled"; + } + return result; } -// -// html_to_text β€” strip HTML for cleaner TOOL:CURL context -// -// Lightweight HTMLβ†’plain-text conversion: -// β€’ Drops , and - for (const std::string &tag : {"script", "style"}) { - std::string open = "<" + tag; - std::string close = ""; - std::string lo = s; - std::transform(lo.begin(), lo.end(), lo.begin(), ::tolower); - for (;;) { - auto p0 = lo.find(open); - if (p0 == std::string::npos) break; - auto p1 = lo.find(close, p0); - if (p1 == std::string::npos) { s.erase(p0); lo.erase(p0); break; } - s.erase(p0, p1 + close.size() - p0); - lo.erase(p0, p1 + close.size() - p0); - } + return true; +} + +bool AgentState::rag_index(const std::string &path, const NitroConfig &cfg, TuiState &tui) { + if (!embed_llama || !rag_db) { + tui.append_line("[err] Load an embedding model first: /embed "); + tui.redraw_all(); + return false; } - // 3. Replace block-level tags with '\n' before stripping all tags. - static const char *const BLOCK[] = { - "p","div","br","li","tr","h1","h2","h3","h4","h5","h6", - "article","section","header","footer","nav","main", nullptr + auto index_one = [&](const std::string &filepath) { + tui.append_line("[sys] indexing: " + filepath); + tui.redraw_all(); + if (!embed_llama->rag_index(*rag_db, filepath)) { + tui.append_line(std::string("[err] rag_load: ") + embed_llama->last_error()); + tui.redraw_all(); + } }; - { - std::string out; - out.reserve(s.size()); - size_t i = 0; - while (i < s.size()) { - if (s[i] != '<') { out += s[i++]; continue; } - auto ce = s.find('>', i); - if (ce == std::string::npos) { out += s[i++]; continue; } - std::string inner = s.substr(i + 1, ce - i - 1); - size_t sp = inner.find_first_of(" \t/\r\n"); - std::string name = (sp != std::string::npos) ? inner.substr(0, sp) : inner; - std::transform(name.begin(), name.end(), name.begin(), ::tolower); - for (int k = 0; BLOCK[k]; ++k) { - if (name == BLOCK[k]) { - out += '\n'; break; - } + + // must be set before indexing + rag_db->embed_dim = embed_llama->get_embed_dim(); + + fs::path rp(path); + std::error_code ec; + if (fs::is_directory(rp, ec)) { + for (const auto &entry : fs::recursive_directory_iterator(rp, ec)) { + if (entry.is_regular_file()) { + index_one(entry.path().string()); } - i = ce + 1; } - s = out; + } else { + index_one(path); } - // 4. Strip all remaining tags. - { - std::string out; out.reserve(s.size()); - bool in_tag = false; - for (char c : s) { - if (c == '<') { in_tag = true; continue; } - if (c == '>') { in_tag = false; continue; } - if (!in_tag) out += c; + std::string save_path = join_path(cfg.sandbox, "rag-index.bin"); + tui.append_line("[sys] saving index: " + save_path); + tui.redraw_all(); + rag_db->save(save_path); + + return true; +} + +// +// Tool dispatch +// +std::string AgentState::process_tool(const std::string &cmd, const NitroConfig &cfg, TuiState &tui) { + const std::string &sandbox = cfg.sandbox; + const std::vector &run_allowed = cfg.run_allowed; + + std::string op, arg1, arg2; + auto sp1 = cmd.find(' '); + if (sp1 == std::string::npos) { + op = trim(cmd); + } else { + op = trim(cmd.substr(0, sp1)); + std::string rest = cmd.substr(sp1 + 1); + rest.erase(0, rest.find_first_not_of(" \t")); + auto sp2 = rest.find(' '); + if (sp2 == std::string::npos) { + arg1 = rest; + } else { + arg1 = rest.substr(0, sp2); + arg2 = rest.substr(sp2 + 1); } - s = out; } - // 5. Decode common HTML entities. - static const std::pair ENT[] = { - {"&","&"},{"<","<"},{">",">"},{""","\""}, - {"'","'"},{" "," "},{"—","β€”"},{"–","–"}, - {"…","…"},{"'","'"},{""","\""}, - {nullptr,nullptr} + auto resolve = [&](const std::string &p) -> std::string { + if (p.empty() || p == ".") { + return sandbox; + } + if (p.substr(0, 2) == "./") { + return join_path(sandbox, p.substr(2)); + } + if (p[0] == '/') { + return p; + } + return join_path(sandbox, disclose(disclose(p, '<', '>'), '[', ']')); }; - for (int k = 0; ENT[k].first; ++k) { - std::string e = ENT[k].first, r = ENT[k].second; - size_t pos = 0; - while ((pos = s.find(e, pos)) != std::string::npos) - { s.replace(pos, e.size(), r); pos += r.size(); } + + tui.append_line("[tool] β†’ " + op); + tui.redraw_all(); + + if (op == "TOOL:DATE") { + char buf[32]; time_t t = time(nullptr); + strftime(buf, sizeof(buf), "%Y-%m-%d", localtime(&t)); + return buf; } - // Numeric entities &#NNN; and &#xHHH; - { - std::string out; out.reserve(s.size()); - size_t i = 0; - while (i < s.size()) { - if (s[i]=='&' && i+2>6)); out += (char)(0x80|(cp&0x3F)); } - else { out += (char)(0xE0|(cp>>12)); out += (char)(0x80|((cp>>6)&0x3F)); out += (char)(0x80|(cp&0x3F)); } - i = semi+1; continue; - } catch (...) {} - } - } - out += s[i++]; + if (op == "TOOL:TIME") { + char buf[32]; time_t t = time(nullptr); + strftime(buf, sizeof(buf), "%H:%M:%S", localtime(&t)); + return buf; + } + if (op == "TOOL:RND") { + return std::to_string((double)rand() / RAND_MAX); + } + if (op == "TOOL:RAG") { + return rag_tool(cfg, arg1); + } + if (op == "TOOL:LIST") { + std::string dir = resolve(arg1); + if (!path_in_sandbox(sandbox, dir)) return "ERROR: path outside sandbox"; + return list_dir(dir); + } + if (op == "TOOL:EXISTS") { + std::string p = resolve(arg1); + if (!path_in_sandbox(sandbox, p)) return "NO"; + return fs::exists(p) ? "YES" : "NO"; + } + if (op == "TOOL:READ") { + std::string p = resolve(arg1); + if (!path_in_sandbox(sandbox, p)) return "ERROR: path outside sandbox"; + return read_file(p); + } + if (op == "TOOL:WRITE") { + std::string p = resolve(arg1); + if (!path_in_sandbox(sandbox, p)) { + return "ERROR: path outside sandbox"; } - s = out; + if (!tui.confirm_dialog(std::format("Allow model to write {}?", p))) { + return "ERROR: action prevented by user"; + } + std::string content = disclose(strip_code_fences(arg1, arg2), '`', '`'); + return write_file(p, content) ? "OK: written to " + arg1 : "ERROR: write failed for " + arg1; } - - // 6. Collapse whitespace; cap blank lines at 2. - { - std::string out; out.reserve(s.size()); - int nl_run = 0; bool last_sp = false; - for (char c : s) { - if (c == '\r') continue; - if (c == '\t') c = ' '; - if (c == '\n') { ++nl_run; last_sp=false; if (nl_run<=2) out+='\n'; continue; } - nl_run = 0; - if (c == ' ') { if (!last_sp) { out+=' '; last_sp=true; } continue; } - last_sp = false; out += c; + if (op == "TOOL:CURL") { + return tool_curl(arg1); + } + if (op == "TOOL:INTROSPECT") { + return introspect(cfg); + } + if (op == "TOOL:RUN") { + std::string prog = resolve(arg1); + if (!path_in_sandbox(sandbox, prog)) { + return "ERROR: path outside sandbox"; + } + if (!run_allowed.empty()) { + std::string basename = fs::path(prog).filename().string(); + bool permitted = std::any_of(run_allowed.begin(), run_allowed.end(), + [&](const std::string &a){ return a == basename; }); + if (!permitted) { + return "ERROR: '" + basename + "' is not in the TOOL:RUN allowlist. " + "Use /set run_allowed to permit it."; + } + } else if (!tui.confirm_dialog(std::format("Allow {} to run?", prog))) { + return "ERROR: prevented by user"; } - size_t f = out.find_first_not_of(" \n"); - size_t l = out.find_last_not_of(" \n"); - s = (f == std::string::npos) ? "" : out.substr(f, l-f+1); + std::string command = prog + " " + arg2 + " 2>&1"; + FILE *fp = popen(command.c_str(), "r"); + if (!fp) { + return "ERROR: popen failed"; + } + std::string out; + char buf[256]; + while (fgets(buf, sizeof(buf), fp)) { + out += buf; + } + pclose(fp); + if (out.size() > 4096) { + out = out.substr(0, 4096) + "\n…(truncated)"; + } + return out; } - return s; + return "ERROR: unknown tool: [" + op + "]"; } // -// TOOL:CURL +// Agent turn // -static size_t curl_write_cb(void *contents, size_t size, size_t nmemb, void *userp) { - std::string *buf = static_cast(userp); - size_t total = size * nmemb; - static constexpr size_t MAX_BODY = 32 * 1024; - if (buf->size() < MAX_BODY) { - size_t room = MAX_BODY - buf->size(); - buf->append(static_cast(contents), std::min(total, room)); +bool AgentState::run_turn(const std::string &user_message, const NitroConfig &cfg, TuiState &tui) { + if (!model_loaded) { + tui.append_line("[err] No model loaded. Use /model "); + tui.redraw_all(); + return false; } - return total; -} - -static std::string tool_curl(const std::string &url) { - if (url.empty()) return "ERROR: TOOL:CURL requires a URL argument"; - CURL *curl = curl_easy_init(); - if (!curl) return "ERROR: curl_easy_init failed"; - std::string body; - body.reserve(4096); - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_cb); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &body); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); - curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5L); - curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L); - curl_easy_setopt(curl, CURLOPT_USERAGENT, "nitro/1.0"); - // Accept compressed responses; curl will decompress automatically. - curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, ""); - - CURLcode res = curl_easy_perform(curl); - long http_code = 0; - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); - - // Query content-type before cleanup (pointer is only valid while handle lives). - char *ct_raw = nullptr; - curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &ct_raw); - std::string content_type = ct_raw ? ct_raw : ""; - std::transform(content_type.begin(), content_type.end(), - content_type.begin(), ::tolower); - curl_easy_cleanup(curl); - if (res != CURLE_OK) { - return std::string("ERROR: curl: ") + curl_easy_strerror(res); + std::string effective_message = user_message; + if (embed_llama && rag_db && rag_session) { + std::string context = embed_llama->rag_retrieve(*rag_db, user_message, cfg.rag_top_k, *rag_session); + if (!context.empty()) { + log_write("RAG: %s", context.c_str()); + effective_message = "Context:\n" + context + "\n\nUser: " + user_message; + } else { + log_write("RAG: no context found [%s]", embed_llama->last_error()); + } } - if (http_code >= 400) { - return "ERROR: HTTP " + std::to_string(http_code) + " from " + url; + if (!iter) { + tui.append_line("[err] Conversation not initialised (call /clear to reset)"); + tui.redraw_all(); + return false; } - if (body.empty()) { - return "(empty response)"; + if (!llama->add_message(*iter, "user", effective_message)) { + tui.append_line(std::string("[err] add_message: ") + llama->last_error()); + tui.redraw_all(); + return false; } + tui.append_line("Nitro: "); - // Strip HTML tags so the model receives clean plain text. - bool is_html = (content_type.find("text/html") != std::string::npos) - || (body.size() > 5 && body.substr(0,5) == " 6 && body.substr(0,6) == ""); - if (is_html) body = html_to_text(body); + // in_think starts false β€” models that don't use blocks emit + // visible text immediately. The spinner activates only while thinking. + enum {t_init, t_think, t_thunk} think_mode = t_init; + tui.set_thinking(false); + std::string buffer; - return body; -} + auto invoke_tool = [&](const std::string &tool, const std::string_view template_str) -> void { + std::string result = process_tool(tool, cfg, tui); + std::string content = std::vformat(template_str, std::make_format_args(result)); + log_write("tool: [%s] result: [%s]", tool.c_str(), result.c_str()); + if (!llama->add_message(*iter, "tool_result", content)) { + tui.append_line(std::string("[err] tool result inject: ") + llama->last_error()); + tui.redraw_all(); + } + if (!iter->_has_next) { + tui.append_line(std::string("[err] failed to evoke tool response: ") + llama->last_error()); + tui.redraw_all(); + } + }; -// -// System prompt -// -static std::string build_system_prompt(const std::vector &knowledge_files, - const std::string &sandbox) { - std::string p; - p += "You are Nitro, an agentic AI assistant for software development.\n" - "Your sandbox (project directory) is: " + sandbox + "\n\n" - "## Tool protocol\n" - " - Emit tool calls on their own new line. for example:\n\n" - "TOOL:LIST\n" - " - The host executes the tool and returns TOOL_RESULT: on the next line.\n\n" - "Available tools:\n" - " TOOL:LIST [dir] list files (default: sandbox root)\n" - " TOOL:READ read file contents\n" - " TOOL:WRITE write text to file\n" - " TOOL:EXISTS YES or NO\n" - " TOOL:RUN [args] run program inside sandbox\n" - " TOOL:DATE current date\n" - " TOOL:TIME current time\n" - " TOOL:RND random float\n" - " TOOL:RAG query the RAG index for additional context\n" - " TOOL:INTROSPECT introspect your settings, top_k etc\n" - " TOOL:CURL HTTP GET; returns response body (max 32 KB)\n\n" - "Rules:\n" - "- Never access files outside the sandbox.\n" - "- Only use one TOOL at a time. Never combine, always use each tool step by step\n" - "- Use TOOL:CURL to fetch documentation, APIs, or web content you need.\n" - "- Reason step-by-step inside <|think|> (hidden from user).\n" - "- After each tool call, explain what you did in plain English.\n\n"; - for (const auto &kf : knowledge_files) { - auto path = join_path(sandbox, kf); - std::ifstream f(path); - if (!f) { - continue; + auto start_think = [&](const std::string &tag) { + if (think_mode == t_init) { + auto pos = buffer.find(tag); + if (pos != std::string::npos) { + think_mode = t_think; + tui.set_thinking(true); + // display prededing text + buffer = buffer.substr(0, pos); + } + } + }; + + auto end_think = [&](const std::string &tag) { + if (think_mode == t_think) { + auto pos = buffer.find(tag); + if (pos != std::string::npos) { + think_mode = t_thunk; + tui.set_thinking(false); + // display remaining text + buffer = buffer.substr(pos + tag.length()); + } + } + }; + + while (iter->_has_next) { + ncinput ni{}; + notcurses_get_nblock(tui.nc, &ni); + if (ni.id == NCKEY_ESC) { + tui.set_thinking(false); + tui.append_line("[err] Generation cancelled by user (Escape)"); + tui.redraw_all(); + return false; + } + std::string tok = llama->next(*iter); + buffer += tok; + if (think_mode == t_init) { + start_think(""); + start_think("<|think|>"); + start_think(""); + start_think("<|channel>thought"); + } + if (think_mode == t_think) { + tui.tick_spinner(); + end_think(""); + end_think(""); + end_think(""); + end_think(""); + } + if (think_mode == t_thunk) { + auto tool_start = buffer.find("TOOL:"); + if (tool_start == 0) { + // fetch all remaining tokens + invoke_tool(trim(buffer + llama->all(*iter)), "TOOL_RESULT: {}"); + buffer.clear(); + think_mode = t_init; + continue; + } + // see https://ai.google.dev/gemma/docs/core/prompt-formatting-gemma4 + tool_start = buffer.find("<|tool_call>call:"); + if (tool_start != std::string::npos) { + buffer += llama->all(*iter); + auto pos = buffer.find_last_not_of("}"); + if (pos != std::string::npos) { + buffer = buffer.substr(0, pos); + } + pos = buffer.find_first_not_of("{"); + if (pos != std::string::npos) { + buffer = buffer.substr(0, pos) + buffer.substr(pos + 1); + } + invoke_tool(trim(buffer), "<|tool_response>{}"); + buffer.clear(); + think_mode = t_init; + continue; + } + auto pos = buffer.find('\n'); + if (pos != std::string::npos) { + tui.append_token(buffer.substr(0, pos + 1)); + buffer = buffer.substr(pos + 1); + } } - log_write("loaded [%s]", path.c_str()); - std::ostringstream oss; - oss << f.rdbuf(); - p += "## Knowledge: " + kf + "\n" + oss.str() + "\n\n"; } - return p; + + if (!buffer.empty()) { + tui.append_token(buffer + "\n"); + } + + tui.flush_token_acc(); + tui.set_thinking(false); + tui.tokens_per_sec = tokens_per_sec(); + LlamaMemoryInfo mem = llama->memory_info(); + tui.kv_used = mem.kv_used; + tui.kv_total = mem.kv_total; + tui.vram_used = mem.vram_used; + tui.vram_total = mem.vram_total; + char stat[128]; + std::snprintf(stat, sizeof(stat), "[sys] %.1f tok/s (%d tokens) KV %.1f%%", + (double)tui.tokens_per_sec, + iter->_tokens_generated, + (double)mem.kv_percent); + tui.append_line(stat); + tui.redraw_all(); + return true; } // @@ -2127,6 +2118,8 @@ static void handle_slash(const std::string &input, tui.redraw_all(); agent.rag_index(path, cfg, tui); } + tui.append_line("[sys] done"); + tui.redraw_all(); return; } @@ -2358,8 +2351,9 @@ int main(int argc, char **argv) { std::string sysp = build_system_prompt(cfg.knowledge_files, cfg.sandbox); agent.reset_conversation(sysp, tui); } - if (!cfg.embed_path.empty()) + if (!cfg.embed_path.empty()) { agent.setup_embed(cfg.embed_path, tui); + } } else { tui.append_line("[sys] No model specified. Use /model to open the file picker,"); tui.append_line("[sys] or /model to load directly."); From 802e78ee61395b95d903652d07146ed1c8d2ca40 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Thu, 28 May 2026 17:42:47 +0930 Subject: [PATCH 124/131] LLAMA: nitro, apply clion suggestions, implement thinking animation --- llama/nitro.cpp | 209 +++++++++++++++++++++++++----------------------- 1 file changed, 108 insertions(+), 101 deletions(-) diff --git a/llama/nitro.cpp b/llama/nitro.cpp index 06d8293..d1b1b2b 100644 --- a/llama/nitro.cpp +++ b/llama/nitro.cpp @@ -46,7 +46,6 @@ #include #include #include -#include #include #include #include @@ -240,16 +239,16 @@ struct TuiState { void destroy(); void resize(); // ── draw ────────────────────────────────────────────────────────── - void redraw_header(); + void redraw_header() const; void redraw_chat(); - void redraw_input(); + void redraw_input() const; void redraw_all(); // ── content helpers ─────────────────────────────────────────────── void append_line(const std::string &line); void append_token(const std::string &token); void flush_token_acc(); // ── interaction ─────────────────────────────────────────────────── - bool confirm_dialog(const std::string &prompt); + bool confirm_dialog(const std::string &prompt) const; // Blocking readline with history navigation, cursor, arrow-key scrolling. std::string readline_blocking(); // Modal popup overlay while a long operation runs. @@ -270,9 +269,9 @@ struct TuiState { // "Model File", "Embedding Model"). // Returns the selected path, or empty string if the user cancelled. std::string file_picker(const std::string &start_dir, - const std::string &title_hint = "File"); + const std::string &title_hint = "File") const; // Legacy alias kept for callers that used the old name. - std::string rag_folder_picker(const std::string &start_dir) { + std::string rag_folder_picker(const std::string &start_dir) const { return file_picker(start_dir, "RAG Folder"); } }; @@ -289,16 +288,16 @@ struct AgentState { bool model_loaded = false; std::string system_prompt; - bool rag_index(const std::string &path, const NitroConfig &cfg, TuiState &tui); - bool rag_load_index(const std::string &path, TuiState &tui); - bool run_turn(const std::string &user_message, const NitroConfig &cfg, TuiState &tui); + bool rag_index(const std::string &path, const NitroConfig &cfg, TuiState &tui) const; + bool rag_load_index(const std::string &path, TuiState &tui) const; + bool run_turn(const std::string &user_message, const NitroConfig &cfg, TuiState &tui) const; bool setup_embed(const std::string &path, TuiState &tui); bool setup_model(const NitroConfig &cfg, TuiState &tui); - void apply_generation_params(const NitroConfig &cfg); + void apply_generation_params(const NitroConfig &cfg) const; void reset_conversation(const std::string &sysprompt, TuiState &tui); - std::string memory_info_text(); - std::string process_tool(const std::string &cmd, const NitroConfig &cfg, TuiState &tui); - std::string rag_tool(const NitroConfig &cfg, const std::string &agent_query); + std::string memory_info_text() const; + std::string process_tool(const std::string &cmd, const NitroConfig &cfg, TuiState &tui) const; + std::string rag_tool(const NitroConfig &cfg, const std::string &agent_query) const; float tokens_per_sec() const; }; @@ -336,7 +335,7 @@ static void log_write(const char *fmt, ...) { } // -// constant for strip_code_fences +// handling for strip_code_fences // static const std::vector CODE_EXTENSIONS = { ".py",".c",".cpp",".h",".bas",".java",".html",".js",".ts", @@ -481,22 +480,6 @@ static void load_settings(NitroConfig &cfg) { settings_get_float(json, "penalty_repeat", cfg.penalty_repeat); } -// Escape a string for embedding in JSON. -static std::string json_escape(const std::string &s) { - std::string out; - out.reserve(s.size() + 4); - for (char c : s) { - switch (c) { - case '"': out += "\\\""; break; - case '\\': out += "\\\\"; break; - case '\n': out += "\\n"; break; - case '\t': out += "\\t"; break; - default: out += c; break; - } - } - return out; -} - static std::string introspect(const NitroConfig &cfg) { static constexpr std::string_view tmpl = "{{\n" @@ -553,7 +536,7 @@ static bool save_settings(const NitroConfig &cfg) { // Trims whitespace from both ends of a string // static std::string trim(std::string_view str) { - const std::string_view whitespace = " \t\n\r\f\v"; + constexpr std::string_view whitespace = " \t\n\r\f\v"; // Find the first non-whitespace character const auto start = str.find_first_not_of(whitespace); @@ -594,10 +577,6 @@ static constexpr uint32_t BG_CHAT_R = 18, BG_CHAT_G = 22, BG_CHAT_B = 30; static constexpr uint32_t BG_INP_R = 22, BG_INP_G = 28, BG_INP_B = 38; static constexpr uint32_t BG_HDR_R = 30, BG_HDR_G = 40, BG_HDR_B = 55; -static inline uint64_t fg_rgb(uint32_t r, uint32_t g, uint32_t b) { - return NCCHANNELS_INITIALIZER(r, g, b, 0, 0, 0); -} - static inline uint64_t chat_ch(uint32_t r, uint32_t g, uint32_t b) { return NCCHANNELS_INITIALIZER(r, g, b, BG_CHAT_R, BG_CHAT_G, BG_CHAT_B); } @@ -711,8 +690,8 @@ static std::string build_system_prompt(const std::vector &knowledge static std::string strip_code_fences(const std::string &filename, const std::string &src) { auto ext = fs::path(filename).extension().string(); - bool is_code = std::any_of(CODE_EXTENSIONS.begin(), CODE_EXTENSIONS.end(), - [&](const std::string &e){ return ext == e; }); + bool is_code = ranges::any_of(CODE_EXTENSIONS, + [&](const std::string &e){ return ext == e; }); if (!is_code) return src; auto pos = src.find("```"); if (pos == std::string::npos) return src; @@ -728,8 +707,8 @@ static std::string strip_code_fences(const std::string &filename, // TOOL:CURL // static size_t curl_write_cb(void *contents, size_t size, size_t nmemb, void *userp) { - std::string *buf = static_cast(userp); - size_t total = size * nmemb; + auto *buf = static_cast(userp); + auto total = size * nmemb; static constexpr size_t MAX_BODY = 32 * 1024; if (buf->size() < MAX_BODY) { size_t room = MAX_BODY - buf->size(); @@ -754,7 +733,7 @@ static std::string html_to_text(const std::string &html) { // 1. Remove … { std::string lo = s; - std::transform(lo.begin(), lo.end(), lo.begin(), ::tolower); + ranges::transform(lo, lo.begin(), ::tolower); auto p0 = lo.find(""); if (p0 != std::string::npos && p1 != std::string::npos) @@ -766,7 +745,7 @@ static std::string html_to_text(const std::string &html) { std::string open = "<" + tag; std::string close = ""; std::string lo = s; - std::transform(lo.begin(), lo.end(), lo.begin(), ::tolower); + ranges::transform(lo, lo.begin(), ::tolower); for (;;) { auto p0 = lo.find(open); if (p0 == std::string::npos) break; @@ -793,7 +772,7 @@ static std::string html_to_text(const std::string &html) { std::string inner = s.substr(i + 1, ce - i - 1); size_t sp = inner.find_first_of(" \t/\r\n"); std::string name = (sp != std::string::npos) ? inner.substr(0, sp) : inner; - std::transform(name.begin(), name.end(), name.begin(), ::tolower); + ranges::transform(name, name.begin(), ::tolower); for (int k = 0; BLOCK[k]; ++k) { if (name == BLOCK[k]) { out += '\n'; break; @@ -897,8 +876,8 @@ static std::string tool_curl(const std::string &url) { char *ct_raw = nullptr; curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &ct_raw); std::string content_type = ct_raw ? ct_raw : ""; - std::transform(content_type.begin(), content_type.end(), - content_type.begin(), ::tolower); + ranges::transform(content_type, + content_type.begin(), ::tolower); curl_easy_cleanup(curl); if (res != CURLE_OK) { return std::string("ERROR: curl: ") + curl_easy_strerror(res); @@ -978,13 +957,14 @@ void TuiState::resize() { // // TuiState::redraw // -void TuiState::redraw_header() { +void TuiState::redraw_header() const { ncplane_erase(header); ncplane_set_base(header, " ", 0, NCCHANNELS_INITIALIZER(BG_HDR_R, BG_HDR_G, BG_HDR_B, BG_HDR_R, BG_HDR_G, BG_HDR_B)); float kv_pct = kv_total > 0 ? 100.f * (float)kv_used / (float)kv_total : 0.f; float vram_pct = vram_total > 0 ? 100.f * (float)vram_used / (float)vram_total : 0.f; + static const char *const SPIN[] = { "β£Ύ","β£½","β£»","β’Ώ","β‘Ώ","⣟","β£―","β£·" }; const char *spin_str = thinking ? SPIN[spinner_frame % 8] : " "; char buf[512]; @@ -1036,38 +1016,61 @@ void TuiState::redraw_chat() { } } -void TuiState::redraw_input() { +void TuiState::redraw_input() const { ncplane_erase(inputpl); - ncplane_set_channels(inputpl, inp_ch(80, 120, 160)); - std::string sep(term_cols, '-'); - ncplane_putstr_yx(inputpl, 0, 0, sep.c_str()); - const std::string prompt = " ❯ "; - const int prompt_cols = 4; - ncplane_set_channels(inputpl, inp_ch(100, 210, 255)); - ncplane_putstr_yx(inputpl, 1, 0, prompt.c_str()); - int max_w = std::max(0, term_cols - prompt_cols - 1); - std::string visible = input_buf; - int view_offset = 0; - if ((int)visible.size() > max_w && max_w > 0) { - view_offset = (int)visible.size() - max_w; - visible = visible.substr(view_offset); - } - int cur_in_view = std::max(0, (int)cursor_pos - view_offset); - cur_in_view = std::min(cur_in_view, (int)visible.size()); - std::string before = visible.substr(0, cur_in_view); - std::string after = cur_in_view < (int)visible.size() - ? visible.substr(cur_in_view + 1) : ""; - char cursor_ch_val = cur_in_view < (int)visible.size() - ? visible[cur_in_view] : ' '; - ncplane_set_channels(inputpl, inp_ch(230, 230, 230)); - ncplane_putstr_yx(inputpl, 1, prompt_cols, before.c_str()); - int cx = prompt_cols + cur_in_view; - ncplane_set_channels(inputpl, NCCHANNELS_INITIALIZER(BG_INP_R, BG_INP_G, BG_INP_B, 180, 230, 255)); - char cbuf[2] = { cursor_ch_val, '\0' }; - ncplane_putstr_yx(inputpl, 1, cx, cbuf); - ncplane_set_channels(inputpl, inp_ch(230, 230, 230)); - if (!after.empty()) { - ncplane_putstr_yx(inputpl, 1, cx + 1, after.c_str()); + + if (thinking) { + static constexpr const char *ROBOT_RIGHT = "πŸ€–βž‘"; + static constexpr const char *ROBOT_LEFT = "β¬…πŸ€–"; + // 15 steps each way = 20 frame cycle + static constexpr int STEPS = 15; + int cycle = spinner_frame % (STEPS * 2); + bool going_right = (cycle < STEPS); + int pos = going_right ? cycle : (STEPS * 2 - 1 - cycle); + + std::string sep(term_cols, '-'); + // blank 4 cols to fit robot + arrow (each emoji is 2 cols wide) + for (int i = pos; i < std::min(pos + 4, term_cols); ++i) sep[i] = ' '; + ncplane_set_channels(inputpl, inp_ch(80, 120, 160)); + ncplane_putstr_yx(inputpl, 0, 0, sep.c_str()); + + ncplane_set_channels(inputpl, NCCHANNELS_INITIALIZER(255, 220, 80, BG_INP_R, BG_INP_G, BG_INP_B)); + ncplane_putstr_yx(inputpl, 0, pos, going_right ? ROBOT_RIGHT : ROBOT_LEFT); + + ncplane_set_channels(inputpl, inp_ch(140, 140, 180)); + ncplane_putstr_yx(inputpl, 1, 2, "thinking…"); + } else { + ncplane_set_channels(inputpl, inp_ch(80, 120, 160)); + std::string sep(term_cols, '-'); + ncplane_putstr_yx(inputpl, 0, 0, sep.c_str()); + const std::string prompt = " ❯ "; + const int prompt_cols = 4; + ncplane_set_channels(inputpl, inp_ch(100, 210, 255)); + ncplane_putstr_yx(inputpl, 1, 0, prompt.c_str()); + int max_w = std::max(0, term_cols - prompt_cols - 1); + std::string visible = input_buf; + int view_offset = 0; + if ((int)visible.size() > max_w && max_w > 0) { + view_offset = (int)visible.size() - max_w; + visible = visible.substr(view_offset); + } + int cur_in_view = std::max(0, (int)cursor_pos - view_offset); + cur_in_view = std::min(cur_in_view, (int)visible.size()); + std::string before = visible.substr(0, cur_in_view); + std::string after = cur_in_view < (int)visible.size() + ? visible.substr(cur_in_view + 1) : ""; + char cursor_ch_val = cur_in_view < (int)visible.size() + ? visible[cur_in_view] : ' '; + ncplane_set_channels(inputpl, inp_ch(230, 230, 230)); + ncplane_putstr_yx(inputpl, 1, prompt_cols, before.c_str()); + int cx = prompt_cols + cur_in_view; + ncplane_set_channels(inputpl, NCCHANNELS_INITIALIZER(BG_INP_R, BG_INP_G, BG_INP_B, 180, 230, 255)); + char cbuf[2] = { cursor_ch_val, '\0' }; + ncplane_putstr_yx(inputpl, 1, cx, cbuf); + ncplane_set_channels(inputpl, inp_ch(230, 230, 230)); + if (!after.empty()) { + ncplane_putstr_yx(inputpl, 1, cx + 1, after.c_str()); + } } } @@ -1081,6 +1084,7 @@ void TuiState::redraw_all() { void TuiState::tick_spinner() { ++spinner_frame; redraw_header(); + redraw_input(); notcurses_render(nc); } @@ -1088,6 +1092,7 @@ void TuiState::set_thinking(bool on) { thinking = on; if (!on) spinner_frame = 0; redraw_header(); + redraw_input(); notcurses_render(nc); } @@ -1242,7 +1247,7 @@ void TuiState::dismiss_modal_popup() { // Returns the chosen path, or "" on cancel. // std::string TuiState::file_picker(const std::string &start_dir, - const std::string &title_hint) { + const std::string &title_hint) const { std::string current_dir = start_dir; { std::error_code ec; @@ -1250,12 +1255,13 @@ std::string TuiState::file_picker(const std::string &start_dir, if (!ec) current_dir = canon.string(); } auto load_entries = [](const std::string &dir, - std::vector &entries) { + std::vector &entries) + { entries.clear(); std::error_code ec; if (fs::path(dir).has_parent_path() && fs::path(dir) != fs::path(dir).root_path()) - entries.push_back(".."); + entries.emplace_back(".."); std::vector dirs, files; for (const auto &e : fs::directory_iterator(dir, ec)) { if (ec) break; @@ -1264,8 +1270,8 @@ std::string TuiState::file_picker(const std::string &start_dir, if (e.is_directory()) dirs.push_back(name); else files.push_back(name); } - std::sort(dirs.begin(), dirs.end()); - std::sort(files.begin(), files.end()); + ranges::sort(dirs); + ranges::sort(files); for (auto &d : dirs) entries.push_back(d + "/"); for (auto &f : files) entries.push_back(f); }; @@ -1427,7 +1433,7 @@ std::string TuiState::file_picker(const std::string &start_dir, // // ─── TuiState::confirm_dialog ───────────────────────────────────────────── // -bool TuiState::confirm_dialog(const std::string &prompt) { +bool TuiState::confirm_dialog(const std::string &prompt) const { ncplane_erase(inputpl); ncplane_set_channels(inputpl, inp_ch(255, 200, 80)); std::string msg = " " + prompt + " [y/n] ❯ "; @@ -1446,7 +1452,7 @@ bool TuiState::confirm_dialog(const std::string &prompt) { notcurses_render(nc); } std::string lo = answer; - std::transform(lo.begin(), lo.end(), lo.begin(), ::tolower); + ranges::transform(lo, lo.begin(), ::tolower); redraw_input(); notcurses_render(nc); return (lo == "y" || lo == "yes" || lo == "sure" || lo == "k"); @@ -1486,7 +1492,7 @@ std::string TuiState::readline_blocking() { // Entering history from a fresh prompt: save current text as draft. std::string hist_entry; if (history.up(hist_entry)) { - if (input_buf.size() > 0 && hist_entry != input_buf) { + if (!input_buf.empty() && hist_entry != input_buf) { // Only save draft when we first leave the bottom of history. // (history.reset_nav was called on entry so the first Up call // always comes from the "new input" position.) @@ -1502,8 +1508,7 @@ std::string TuiState::readline_blocking() { if (ni.id == NCKEY_DOWN) { std::string hist_entry; - bool got = history.down(hist_entry); - if (got) { + if (history.down(hist_entry)) { input_buf = hist_entry; cursor_pos = input_buf.size(); } else { @@ -1581,7 +1586,7 @@ std::string TuiState::readline_blocking() { } } -void AgentState::apply_generation_params(const NitroConfig &cfg) { +void AgentState::apply_generation_params(const NitroConfig &cfg) const { llama->add_stop("<|turn|>"); llama->add_stop("<|im_end|>"); llama->set_max_tokens(cfg.n_max_tokens); @@ -1672,7 +1677,7 @@ float AgentState::tokens_per_sec() const { return (float)(iter->_tokens_generated / elapsed); } -std::string AgentState::memory_info_text() { +std::string AgentState::memory_info_text() const { if (!model_loaded) return "No model loaded."; LlamaMemoryInfo m = llama->memory_info(); std::ostringstream oss; @@ -1688,7 +1693,7 @@ std::string AgentState::memory_info_text() { return oss.str(); } -std::string AgentState::rag_tool(const NitroConfig &cfg, const std::string &agent_query) { +std::string AgentState::rag_tool(const NitroConfig &cfg, const std::string &agent_query) const { std::string result; if (embed_llama && rag_db && rag_session) { result = embed_llama->rag_retrieve(*rag_db, agent_query, cfg.rag_top_k, *rag_session); @@ -1701,7 +1706,7 @@ std::string AgentState::rag_tool(const NitroConfig &cfg, const std::string &agen return result; } -bool AgentState::rag_load_index(const std::string &path, TuiState &tui) { +bool AgentState::rag_load_index(const std::string &path, TuiState &tui) const { if (!embed_llama || !rag_db) { tui.append_line("[err] Load an embedding model first: /embed "); tui.redraw_all(); @@ -1716,7 +1721,7 @@ bool AgentState::rag_load_index(const std::string &path, TuiState &tui) { return true; } -bool AgentState::rag_index(const std::string &path, const NitroConfig &cfg, TuiState &tui) { +bool AgentState::rag_index(const std::string &path, const NitroConfig &cfg, TuiState &tui) const { if (!embed_llama || !rag_db) { tui.append_line("[err] Load an embedding model first: /embed "); tui.redraw_all(); @@ -1758,7 +1763,7 @@ bool AgentState::rag_index(const std::string &path, const NitroConfig &cfg, TuiS // // Tool dispatch // -std::string AgentState::process_tool(const std::string &cmd, const NitroConfig &cfg, TuiState &tui) { +std::string AgentState::process_tool(const std::string &cmd, const NitroConfig &cfg, TuiState &tui) const { const std::string &sandbox = cfg.sandbox; const std::vector &run_allowed = cfg.run_allowed; @@ -1850,8 +1855,8 @@ std::string AgentState::process_tool(const std::string &cmd, const NitroConfig & } if (!run_allowed.empty()) { std::string basename = fs::path(prog).filename().string(); - bool permitted = std::any_of(run_allowed.begin(), run_allowed.end(), - [&](const std::string &a){ return a == basename; }); + bool permitted = ranges::any_of(run_allowed, + [&](const std::string &a){ return a == basename; }); if (!permitted) { return "ERROR: '" + basename + "' is not in the TOOL:RUN allowlist. " "Use /set run_allowed to permit it."; @@ -1881,7 +1886,7 @@ std::string AgentState::process_tool(const std::string &cmd, const NitroConfig & // // Agent turn // -bool AgentState::run_turn(const std::string &user_message, const NitroConfig &cfg, TuiState &tui) { +bool AgentState::run_turn(const std::string &user_message, const NitroConfig &cfg, TuiState &tui) const { if (!model_loaded) { tui.append_line("[err] No model loaded. Use /model "); tui.redraw_all(); @@ -1916,6 +1921,7 @@ bool AgentState::run_turn(const std::string &user_message, const NitroConfig &cf std::string buffer; auto invoke_tool = [&](const std::string &tool, const std::string_view template_str) -> void { + log_write("tool request: [%s]", tool.c_str()); std::string result = process_tool(tool, cfg, tui); std::string content = std::vformat(template_str, std::make_format_args(result)); log_write("tool: [%s] result: [%s]", tool.c_str(), result.c_str()); @@ -1994,7 +2000,7 @@ bool AgentState::run_turn(const std::string &user_message, const NitroConfig &cf if (pos != std::string::npos) { buffer = buffer.substr(0, pos); } - pos = buffer.find_first_not_of("{"); + pos = buffer.find_first_not_of('{'); if (pos != std::string::npos) { buffer = buffer.substr(0, pos) + buffer.substr(pos + 1); } @@ -2258,14 +2264,15 @@ int main(int argc, char **argv) { // ── Parse arguments (command-line overrides saved settings) ────── load_settings(cfg); auto resolve_path = [](const std::string &arg) -> std::string { - std::error_code ec; if (arg.substr(0, 2) == "~/") { const char *home = getenv("HOME"); return std::string(home ? home : ".") + "/" + arg.substr(2); } - if (arg.substr(0, 2) == "./") { - return (fs::current_path(ec) / arg.substr(2)).string(); - } + if (arg.substr(0, 2) == "./") + { + std::error_code ec; + return (fs::current_path(ec) / arg.substr(2)).string(); + } return arg; }; @@ -2329,7 +2336,7 @@ int main(int argc, char **argv) { // ── Auto-discover knowledge files ───────────────────────────────── for (const char *kf : {"nitro.md", "AGENTS.md", "README.md"}) { - if (fs::exists(kf)) cfg.knowledge_files.push_back(kf); + if (fs::exists(kf)) cfg.knowledge_files.emplace_back(kf); } // ── Init curl globally ──────────────────────────────────────────── From 66c28757ad17c28317ae003543c22a61aeefd063 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Thu, 28 May 2026 21:24:02 +0930 Subject: [PATCH 125/131] LLAMA: nitro added mkdir tool --- llama/nitro.cpp | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/llama/nitro.cpp b/llama/nitro.cpp index d1b1b2b..9b47e20 100644 --- a/llama/nitro.cpp +++ b/llama/nitro.cpp @@ -643,6 +643,21 @@ static bool write_file(const std::string &path, const std::string &data) { return f.good(); } +static bool make_dir(const std::string &path) { + try { + std::filesystem::path p(path); + if (fs::exists(p)) { + return true; + } + std::error_code ec; + return fs::create_directories(p, ec); + } + catch (const std::filesystem::filesystem_error &e) { + log_write("mkdir failed [%s]", e.what()); + return false; + } +} + // // System prompt // @@ -659,6 +674,7 @@ static std::string build_system_prompt(const std::vector &knowledge " TOOL:LIST [dir] list files (default: sandbox root)\n" " TOOL:READ read file contents\n" " TOOL:WRITE write text to file\n" + " TOOL:MKDIR create a subfolder inside the sandbox\n" " TOOL:EXISTS YES or NO\n" " TOOL:RUN [args] run program inside sandbox\n" " TOOL:DATE current date\n" @@ -1768,7 +1784,7 @@ std::string AgentState::process_tool(const std::string &cmd, const NitroConfig & const std::vector &run_allowed = cfg.run_allowed; std::string op, arg1, arg2; - auto sp1 = cmd.find(' '); + auto sp1 = cmd.find_first_of(" \n"); if (sp1 == std::string::npos) { op = trim(cmd); } else { @@ -1839,9 +1855,16 @@ std::string AgentState::process_tool(const std::string &cmd, const NitroConfig & if (!tui.confirm_dialog(std::format("Allow model to write {}?", p))) { return "ERROR: action prevented by user"; } - std::string content = disclose(strip_code_fences(arg1, arg2), '`', '`'); + std::string content = disclose(disclose(strip_code_fences(arg1, arg2), '`', '`'), '"', '"'); return write_file(p, content) ? "OK: written to " + arg1 : "ERROR: write failed for " + arg1; } + if (op == "TOOL:MKDIR") { + std::string p = resolve(arg1); + if (!path_in_sandbox(sandbox, p)) { + return "ERROR: path outside sandbox"; + } + return make_dir(p) ? "OK: created " + arg1 : "ERROR: mkdir failed for " + arg1; + } if (op == "TOOL:CURL") { return tool_curl(arg1); } @@ -1849,22 +1872,16 @@ std::string AgentState::process_tool(const std::string &cmd, const NitroConfig & return introspect(cfg); } if (op == "TOOL:RUN") { - std::string prog = resolve(arg1); - if (!path_in_sandbox(sandbox, prog)) { - return "ERROR: path outside sandbox"; - } if (!run_allowed.empty()) { - std::string basename = fs::path(prog).filename().string(); - bool permitted = ranges::any_of(run_allowed, - [&](const std::string &a){ return a == basename; }); + bool permitted = ranges::any_of(run_allowed, [&](const std::string &a) {return a == arg1;}); if (!permitted) { - return "ERROR: '" + basename + "' is not in the TOOL:RUN allowlist. " + return "ERROR: '" + arg1 + "' is not in the TOOL:RUN allowlist. " "Use /set run_allowed to permit it."; } - } else if (!tui.confirm_dialog(std::format("Allow {} to run?", prog))) { + } else if (!tui.confirm_dialog(std::format("Allow {} {} to run?", arg1, arg2))) { return "ERROR: prevented by user"; } - std::string command = prog + " " + arg2 + " 2>&1"; + std::string command = arg1 + " " + arg2 + " 2>&1"; FILE *fp = popen(command.c_str(), "r"); if (!fp) { return "ERROR: popen failed"; From 638984923d7840d9e383d1b65b62f3f44941b265 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Thu, 28 May 2026 21:29:00 +0930 Subject: [PATCH 126/131] LLAMA: code cleanup --- llama/chunk_headers.cpp | 312 ---------------------------------------- llama/rag_index.cpp | 206 -------------------------- 2 files changed, 518 deletions(-) delete mode 100644 llama/chunk_headers.cpp delete mode 100644 llama/rag_index.cpp diff --git a/llama/chunk_headers.cpp b/llama/chunk_headers.cpp deleted file mode 100644 index 0d8eff0..0000000 --- a/llama/chunk_headers.cpp +++ /dev/null @@ -1,312 +0,0 @@ -/* - * chunk_headers.cpp - * - * Smart chunker for C/C++ headers β€” keeps semantic units together: - * - block comment + following declaration/function - * - struct/enum/typedef blocks - * - grouped #define macros - * - standalone inline-commented declarations - * - * Output: one chunk per line in a .jsonl file: - * {"source":"notcurses.h","type":"function","text":"..."} - * - * Build: c++ -std=c++17 -o chunk_headers chunk_headers.cpp - * Usage: ./chunk_headers notcurses/include/notcurses/notcurses.h > chunks.jsonl - * ./chunk_headers dir/ > chunks.jsonl - */ - -#include -#include -#include -#include -#include -#include -#include - -namespace fs = std::filesystem; - -/* ── tunables ──────────────────────────────────────────────── */ -static constexpr size_t MIN_CHUNK = 40; /* ignore tiny fragments */ -/* ─────────────────────────────────────────────────────────── */ - -enum class ChunkType { - Function, Struct, Enum, Typedef, Defines, Other -}; - -static std::string type_name(ChunkType t) { - switch (t) { - case ChunkType::Function: return "function"; - case ChunkType::Struct: return "struct"; - case ChunkType::Enum: return "enum"; - case ChunkType::Typedef: return "typedef"; - case ChunkType::Defines: return "defines"; - default: return "other"; - } -} - -/* ── helpers ───────────────────────────────────────────────── */ - -static bool starts_with(const std::string &s, const std::string &prefix) { - return s.size() >= prefix.size() && - s.compare(0, prefix.size(), prefix) == 0; -} - -static bool is_blank(const std::string &s) { - for (char c : s) if (!isspace((unsigned char)c)) return false; - return true; -} - -static std::string json_escape(const std::string &in) { - std::string out; - out.reserve(in.size() + 32); - for (unsigned char c : in) { - if (c == '"') { out += "\\\""; } - else if (c == '\\') { out += "\\\\"; } - else if (c == '\n') { out += "\\n"; } - else if (c == '\r') { /* skip */ } - else if (c == '\t') { out += "\\t"; } - else if (c < 0x20) { /* skip */ } - else { out += c; } - } - return out; -} - -static void emit_chunk(const std::string &source, ChunkType type, - const std::string &text) { - if (text.size() < MIN_CHUNK) return; - - /* trim trailing newlines */ - size_t end = text.size(); - while (end > 0 && (text[end-1] == '\n' || text[end-1] == '\r')) --end; - if (end < MIN_CHUNK) return; - - std::cout << "{\"source\":\"" << json_escape(source) - << "\",\"type\":\"" << type_name(type) - << "\",\"text\":\"" << json_escape(text.substr(0, end)) - << "\"}\n"; -} - -/* ── state machine ─────────────────────────────────────────── */ - -enum class State { - Idle, BlockComment, LineComment, Declaration, Struct, Defines -}; - -static void process_file(const fs::path &path) { - std::ifstream f(path); - if (!f) { std::cerr << "cannot open: " << path << "\n"; return; } - - const std::string source = path.filename().string(); - - State state = State::Idle; - std::string chunk; - ChunkType chunk_type = ChunkType::Other; - int brace_depth = 0; - int paren_depth = 0; - int define_count = 0; - - auto flush = [&](ChunkType t) { - emit_chunk(source, t, chunk); - chunk.clear(); - state = State::Idle; - brace_depth = 0; - paren_depth = 0; - }; - - std::string line; - while (std::getline(f, line)) { - /* trim trailing CR */ - if (!line.empty() && line.back() == '\r') line.pop_back(); - - /* find first non-whitespace for prefix checks */ - size_t trim_pos = 0; - while (trim_pos < line.size() && - (line[trim_pos] == ' ' || line[trim_pos] == '\t')) ++trim_pos; - const std::string trimmed = line.substr(trim_pos); - - /* ── #define handling ─────────────────────────────────── */ - if (starts_with(trimmed, "#define ")) { - if (state == State::BlockComment || state == State::LineComment) { - chunk += line + "\n"; - state = State::Defines; - define_count = 1; - } else if (state == State::Defines) { - chunk += line + "\n"; - define_count++; - } else { - if (chunk.size() >= MIN_CHUNK) emit_chunk(source, chunk_type, chunk); - chunk.clear(); - chunk += line + "\n"; - state = State::Defines; - define_count = 1; - } - continue; - } - - /* non-define while in define group */ - if (state == State::Defines) { - flush(ChunkType::Defines); - define_count = 0; - /* fall through to process this line normally */ - } - - /* ── block comment start ──────────────────────────────── */ - if ((starts_with(trimmed, "/*") || starts_with(trimmed, "/**")) && - state == State::Idle) { - if (chunk.size() >= MIN_CHUNK) emit_chunk(source, chunk_type, chunk); - chunk.clear(); - chunk_type = ChunkType::Other; - chunk += line + "\n"; - state = (trimmed.find("*/", 2) != std::string::npos) - ? State::LineComment - : State::BlockComment; - continue; - } - - /* ── inside block comment ─────────────────────────────── */ - if (state == State::BlockComment) { - chunk += line + "\n"; - if (trimmed.find("*/") != std::string::npos) - state = State::LineComment; - continue; - } - - /* ── // line comment ──────────────────────────────────── */ - if (starts_with(trimmed, "//")) { - if (state == State::Idle) { - if (chunk.size() >= MIN_CHUNK) emit_chunk(source, chunk_type, chunk); - chunk.clear(); - chunk += line + "\n"; - state = State::LineComment; - } else if (state == State::LineComment) { - chunk += line + "\n"; - } - continue; - } - - /* ── blank line ───────────────────────────────────────── */ - if (is_blank(trimmed)) { - if (state == State::LineComment) - flush(ChunkType::Other); - else if (state == State::Idle && chunk.size() >= MIN_CHUNK) - flush(chunk_type); - continue; - } - - /* ── skip preprocessor noise ──────────────────────────── */ - if (starts_with(trimmed, "#ifndef") || starts_with(trimmed, "#ifdef") || - starts_with(trimmed, "#endif") || starts_with(trimmed, "#pragma") || - starts_with(trimmed, "#include")) { - if (state == State::LineComment || state == State::BlockComment) { - chunk.clear(); - state = State::Idle; - } - continue; - } - - /* ── typedef struct / enum start ─────────────────────── */ - if ((starts_with(trimmed, "typedef struct") || - starts_with(trimmed, "typedef enum") || - starts_with(trimmed, "struct ") || - starts_with(trimmed, "enum ")) && - (state == State::Idle || state == State::LineComment)) { - - if (state == State::Idle && chunk.size() >= MIN_CHUNK) - emit_chunk(source, chunk_type, chunk); - - /* preserve any comment already in chunk */ - if (state == State::Idle) chunk.clear(); - - chunk += line + "\n"; - chunk_type = starts_with(trimmed, "typedef") ? ChunkType::Typedef - : starts_with(trimmed, "enum ") ? ChunkType::Enum - : ChunkType::Struct; - state = State::Struct; - for (char c : line) { - if (c == '{') ++brace_depth; - if (c == '}') --brace_depth; - } - if (brace_depth <= 0 && line.find(';') != std::string::npos) - flush(chunk_type); - continue; - } - - /* ── inside struct/enum body ──────────────────────────── */ - if (state == State::Struct) { - chunk += line + "\n"; - for (char c : line) { - if (c == '{') ++brace_depth; - if (c == '}') --brace_depth; - } - if (brace_depth <= 0 && line.find(';') != std::string::npos) - flush(chunk_type); - continue; - } - - /* ── function / other declaration ────────────────────── */ - if (state == State::LineComment || state == State::Idle) { - if (state == State::Idle && chunk.size() >= MIN_CHUNK) { - emit_chunk(source, chunk_type, chunk); - chunk.clear(); - } - chunk += line + "\n"; - chunk_type = ChunkType::Function; - state = State::Declaration; - for (char c : line) { - if (c == '(') ++paren_depth; - if (c == ')') --paren_depth; - } - if (paren_depth <= 0 && line.find(';') != std::string::npos) - flush(ChunkType::Function); - continue; - } - - /* ── multi-line declaration ───────────────────────────── */ - if (state == State::Declaration) { - chunk += line + "\n"; - for (char c : line) { - if (c == '(') ++paren_depth; - if (c == ')') --paren_depth; - } - if (paren_depth <= 0 && line.find(';') != std::string::npos) - flush(ChunkType::Function); - continue; - } - } - - /* flush remainder */ - if (chunk.size() >= MIN_CHUNK) emit_chunk(source, chunk_type, chunk); -} - -/* ── directory walker ──────────────────────────────────────── */ - -static void process_path(const fs::path &path) { - if (fs::is_directory(path)) { - /* sorted for deterministic output */ - std::vector entries; - for (auto &e : fs::recursive_directory_iterator(path)) - entries.push_back(e.path()); - std::sort(entries.begin(), entries.end()); - for (auto &e : entries) { - if (!fs::is_regular_file(e)) continue; - auto ext = e.extension().string(); - if (ext == ".h" || ext == ".hpp" || ext == ".c" || ext == ".cpp") - process_file(e); - } - } else if (fs::is_regular_file(path)) { - process_file(path); - } -} - -/* ── main ──────────────────────────────────────────────────── */ - -int main(int argc, char **argv) { - if (argc < 2) { - std::cerr << "usage: " << argv[0] - << " [header2.h ...]\n"; - return 1; - } - for (int i = 1; i < argc; i++) - process_path(fs::path(argv[i])); - return 0; -} diff --git a/llama/rag_index.cpp b/llama/rag_index.cpp deleted file mode 100644 index 9f8a4fe..0000000 --- a/llama/rag_index.cpp +++ /dev/null @@ -1,206 +0,0 @@ -/* - * rag_index.cpp - * - * Reads chunks.jsonl produced by chunk_headers, embeds each chunk - * using a GGUF embedding model via llama.h, saves a binary .db file. - * - * No fixed limits on chunk count or chunk length. - * - * Build: - * c++ -std=c++17 -o rag_index rag_index.cpp -lllama -lm - * - * Usage: - * ./rag_index \ - * --model nomic-embed-text-v1.5.Q4_K_M.gguf \ - * --input chunks.jsonl \ - * --output notcurses.db - */ - -#include "llama-sb.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -/* ── tunables ──────────────────────────────────────────────── */ -static constexpr int BATCH_SIZE = 512; -/* ─────────────────────────────────────────────────────────── */ - -/* ── on-disk chunk (variable-length text) ──────────────────── */ -/* - * db header (16 bytes): - * uint32 magic = 0x52414744 "RAGD" - * uint32 version = 2 - * uint32 n_chunks - * uint32 embed_dim - * - * per chunk: - * uint32 text_len - * char[] text (text_len bytes, no null) - * uint16 source_len - * char[] source (source_len bytes, no null) - * uint8 type_len - * char[] type (type_len bytes, no null) - * float[] embedding (embed_dim floats) - */ - -struct Chunk { - std::string text; - std::string source; - std::string type; - std::vector embedding; -}; - -/* ── tiny JSON string extractor ────────────────────────────── */ -static bool json_get_string(const std::string &json, - const std::string &key, - std::string &out) { - std::string search = "\"" + key + "\":"; - size_t pos = json.find(search); - if (pos == std::string::npos) return false; - pos += search.size(); - while (pos < json.size() && json[pos] == ' ') ++pos; - if (pos >= json.size() || json[pos] != '"') return false; - ++pos; /* skip opening quote */ - out.clear(); - while (pos < json.size()) { - char c = json[pos++]; - if (c == '\\' && pos < json.size()) { - char e = json[pos++]; - switch (e) { - case 'n': out += '\n'; break; - case 't': out += '\t'; break; - case '"': out += '"'; break; - case '\\': out += '\\'; break; - default: out += e; break; - } - } else if (c == '"') { - break; - } else { - out += c; - } - } - return true; -} - -/* ── db save ───────────────────────────────────────────────── */ -static bool save_db(const std::string &path, - const std::vector &chunks, - int embed_dim) { - std::ofstream f(path, std::ios::binary); - if (!f) { std::cerr << "cannot open for write: " << path << "\n"; return false; } - - auto write32 = [&](uint32_t v) { f.write((char*)&v, 4); }; - auto write16 = [&](uint16_t v) { f.write((char*)&v, 2); }; - auto write8 = [&](uint8_t v) { f.write((char*)&v, 1); }; - auto writestr = [&](const std::string &s, size_t max_len) { - size_t len = std::min(s.size(), max_len); - f.write(s.c_str(), (std::streamsize)len); - }; - - write32(0x52414744); /* magic "RAGD" */ - write32(2); /* version */ - write32((uint32_t)chunks.size()); /* n_chunks */ - write32((uint32_t)embed_dim); /* embed_dim */ - - for (const Chunk &c : chunks) { - write32((uint32_t)c.text.size()); - f.write(c.text.c_str(), (std::streamsize)c.text.size()); - - uint16_t src_len = (uint16_t)std::min(c.source.size(), (size_t)65535); - write16(src_len); - writestr(c.source, src_len); - - uint8_t type_len = (uint8_t)std::min(c.type.size(), (size_t)255); - write8(type_len); - writestr(c.type, type_len); - - f.write((char*)c.embedding.data(), - (std::streamsize)(embed_dim * sizeof(float))); - } - - return f.good(); -} - -/* ── main ──────────────────────────────────────────────────── */ -int main(int argc, char **argv) { - std::string model_path; - std::string input_path; - std::string output_path = "corpus.db"; - - for (int i = 1; i < argc; i++) { - if (!strcmp(argv[i], "--model") && i+1 < argc) model_path = argv[++i]; - if (!strcmp(argv[i], "--input") && i+1 < argc) input_path = argv[++i]; - if (!strcmp(argv[i], "--output") && i+1 < argc) output_path = argv[++i]; - } - - if (model_path.empty() || input_path.empty()) { - std::cerr << "usage: rag_index --model " - "--input [--output ]\n"; - return 1; - } - - /* ── load embedding model ─────────────────────────────── */ - - Llama llama; - if (!llama.load_embedding_model(model_path)) { - return 1; - } - - int embed_dim = llama.get_embed_dim(); - std::cerr << "embedding dim: " << embed_dim << "\n"; - - /* ── read and embed chunks ────────────────────────────── */ - std::vector chunks; - std::ifstream fin(input_path); - if (!fin) { - std::cerr << "cannot open: " << input_path << "\n"; - return 1; - } - - std::string line; - int skipped = 0; - - while (std::getline(fin, line)) { - if (line.empty() || line[0] != '{') { - continue; - } - - Chunk c; - if (!json_get_string(line, "text", c.text) || - !json_get_string(line, "source", c.source)) { - ++skipped; - continue; - } - json_get_string(line, "type", c.type); - - std::cerr << "\r[" << chunks.size() << "] embedding: " - << c.text.substr(0, 40) << "..."; - - std::string text = "Instruct: Represent this API documentation for code retrieval\nQuery: " + c.text; - if (!llama.embed_text(text, c.embedding, embed_dim)) { - ++skipped; - continue; - } - - chunks.push_back(std::move(c)); - } - std::cerr << "\n"; - std::cerr << "embedded " << chunks.size() - << " chunks (" << skipped << " skipped)\n"; - - /* ── save ─────────────────────────────────────────────── */ - if (!save_db(output_path, chunks, embed_dim)) { - std::cerr << "failed to save db\n"; - return 1; - } - std::cerr << "saved β†’ " << output_path << "\n"; - - return 0; -} From 8406b7f05961a4864a7396d3086f5aba17b6c492 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Fri, 29 May 2026 19:44:49 +0930 Subject: [PATCH 127/131] LLAMA: nitro read local settings, replace disclose with unwrap fn --- llama/nitro.cpp | 116 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 96 insertions(+), 20 deletions(-) diff --git a/llama/nitro.cpp b/llama/nitro.cpp index 9b47e20..fcfa3fd 100644 --- a/llama/nitro.cpp +++ b/llama/nitro.cpp @@ -50,8 +50,11 @@ #include #include #include +#include + #include "llama-sb.h" #include "llama-sb-rag.h" + #include namespace fs = std::filesystem; @@ -344,12 +347,13 @@ static const std::vector CODE_EXTENSIONS = { // // Settings persistence (~/.config/nitro/nitro.settings.json) -// -// A minimal hand-rolled JSON reader/writer for the flat key-value settings -// we care about. We deliberately avoid a full JSON library dependency. - // Returns the canonical settings path: ~/.config/nitro/settings.json +// static std::string settings_path() { + // Attempt to read settings from the current working directory first + if (fs::exists("settings.json")) { + return "settings.json"; + } const char *home = getenv("HOME"); std::string base = home ? std::string(home) : "."; return base + "/.config/nitro/settings.json"; @@ -362,6 +366,10 @@ static std::string history_path() { return base + "/.config/nitro/history.txt"; } +// +// A minimal hand-rolled JSON reader/writer for the flat key-value settings +// we care about. We deliberately avoid a full JSON library dependency. +// static bool json_get_string(const std::string &json, const std::string &key, std::string &out) { @@ -551,25 +559,93 @@ static std::string trim(std::string_view str) { return std::string(str.substr(start, end - start + 1)); } -// -// Removes any front and back characters -// -static std::string disclose(const std::string &input, char c1, char c2) { - // Check if string has at least 2 characters - if (input.length() < 2) { +/* + * unwrap() - Remove a matching outer "wrapper" from a string. + * + * Trims leading/trailing whitespace first, then checks (in order): + * + * 1. Same-character pairs "..." '...' |...| `...` + * 2. Mirror pairs (...) [...] {...} + * 3. HTML-like tags ... + * 4. Plain angle brackets <...> (fallback if tags don't match) + * + * If none of the above apply, returns the whitespace-trimmed input unchanged. + * + * Examples: + * unwrap("\"hello\"") -> "hello" + * unwrap(" [foo] ") -> "foo" + * unwrap("bold") -> "bold" + * unwrap("x") -> "x" + * unwrap("") -> "hello" + * unwrap("plain") -> "plain" + * unwrap("") -> "" + */ +std::string unwrap(const std::string &input) { + if (input.empty()) { return input; } + + size_t left = 0; + size_t right = input.length() - 1; + + while (left <= right && std::isspace(static_cast(input[left]))) { + left++; + } + while (left <= right && std::isspace(static_cast(input[right]))) { + right--; + } + + if (left > right) { + return ""; + } + + // Same-character pairs: "", '', ||, `` + // Note: [], {} are NOT same-char pairs β€” they belong in mirror pairs only + if (input[left] == input[right]) { + if (input[left] == '"' || input[left] == '\'' || + input[left] == '|' || input[left] == '`') { + return input.substr(left + 1, right - left - 1); + } + } + + // Mirror pairs: (), [], {}, but NOT <> (handled below as possible HTML tags) + if (input[left] != input[right]) { + if ((input[left] == '(' && input[right] == ')') || + (input[left] == '[' && input[right] == ']') || + (input[left] == '{' && input[right] == '}')) { + return input.substr(left + 1, right - left - 1); + } + } + + // HTML-like tags: content + // Also handles plain <...> as a fallback at the end + if (input[left] == '<' && input[right] == '>') { + // Find end of opening tag + size_t openTagEnd = left + 1; + while (openTagEnd <= right && input[openTagEnd] != '>') openTagEnd++; + + if (openTagEnd < right) { + std::string openTagName = input.substr(left + 1, openTagEnd - left - 1); + + // Find start of closing tag (search backwards for '<') + size_t closeTagStart = right; + while (closeTagStart > openTagEnd && input[closeTagStart] != '<') closeTagStart--; + + if (closeTagStart > openTagEnd && input[closeTagStart + 1] == '/') { + std::string closeTagName = input.substr(closeTagStart + 2, right - closeTagStart - 2); + + if (!openTagName.empty() && openTagName == closeTagName) { + // Return content between the tags + return input.substr(openTagEnd + 1, closeTagStart - openTagEnd - 1); + } + } + } - // Check if first and last characters match the specified delimiters - if (input[0] == c1 && input[input.length() - 1] == c2) { - // Remove first and last characters - std::string result = input; - result.erase(0, 1); - result.erase(input.length() - 1, 1); - return result; + // Fallback: plain <...> with no matching HTML tags β€” unwrap the angle brackets + return input.substr(left + 1, right - left - 1); } - return input; + return input.substr(left, right - left + 1); } // ─── colour helpers ────────────────────────────────────────────────────── @@ -1810,7 +1886,7 @@ std::string AgentState::process_tool(const std::string &cmd, const NitroConfig & if (p[0] == '/') { return p; } - return join_path(sandbox, disclose(disclose(p, '<', '>'), '[', ']')); + return join_path(sandbox, unwrap(p)); }; tui.append_line("[tool] β†’ " + op); @@ -1855,7 +1931,7 @@ std::string AgentState::process_tool(const std::string &cmd, const NitroConfig & if (!tui.confirm_dialog(std::format("Allow model to write {}?", p))) { return "ERROR: action prevented by user"; } - std::string content = disclose(disclose(strip_code_fences(arg1, arg2), '`', '`'), '"', '"'); + std::string content = unwrap(strip_code_fences(arg1, arg2)); return write_file(p, content) ? "OK: written to " + arg1 : "ERROR: write failed for " + arg1; } if (op == "TOOL:MKDIR") { From 966cda00075fa8d4fa80f35738f25cc0315a4fb0 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Sat, 30 May 2026 11:41:18 +0930 Subject: [PATCH 128/131] LLAMA: nitro - update prompt --- llama/nitro.cpp | 131 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 104 insertions(+), 27 deletions(-) diff --git a/llama/nitro.cpp b/llama/nitro.cpp index fcfa3fd..3ce5629 100644 --- a/llama/nitro.cpp +++ b/llama/nitro.cpp @@ -79,6 +79,7 @@ struct NitroConfig { int penalty_last_n = 256; std::vector knowledge_files; int rag_top_k = 5; + bool thinking = true; // TOOL:RUN allowlist β€” if non-empty, only these program basenames may run. // Empty means "allow anything inside the sandbox" (original behaviour). std::vector run_allowed; @@ -467,6 +468,8 @@ static void load_settings(NitroConfig &cfg) { std::ostringstream oss; oss << f.rdbuf(); std::string json = oss.str(); + cfg.thinking = true; + // String fields settings_get_str(json, "model_path", cfg.model_path); settings_get_str(json, "embed_path", cfg.embed_path); @@ -584,7 +587,7 @@ std::string unwrap(const std::string &input) { if (input.empty()) { return input; } - + size_t left = 0; size_t right = input.length() - 1; @@ -740,13 +743,46 @@ static bool make_dir(const std::string &path) { static std::string build_system_prompt(const std::vector &knowledge_files, const std::string &sandbox) { std::string p; - p += "You are Nitro, an agentic AI assistant for software development.\n" + p += + "You are Nitro, an agentic AI assistant for software development. " + "Proceed with caution, guided by logic and the pursuit of knowledge.\n\n" + "Your sandbox (project directory) is: " + sandbox + "\n\n" - "## Tool protocol\n" - " - Emit tool calls on their own new line. for example:\n\n" + + "## Core Principle\n" + "Always follow this loop: THINK β†’ DECIDE β†’ ACT β†’ RESPOND\n\n" + + "## Reasoning Protocol\n" + "Use <|think|> to reason BEFORE acting. Keep it concise and structured.\n" + "Format:\n" + "<|think|>\n" + "- What is the user asking?\n" + "- Do I need external data (files, tools)?\n" + "- What is the safest and most correct action?\n" + "\n\n" + "Rules:\n" + "- Do NOT call tools inside <|think|>\n" + "- Do NOT include the final answer inside <|think|>\n" + "- Always follow <|think|> with either a tool call OR a final answer\n" + "- Skip <|think|> only for trivial or conversational responses\n\n" + + "## Tool Protocol\n" + "Emit ONE tool call at a time, immediately followed by NITRO_END_TOOL.\n" + "Do NOT add any commentary, explanation, or text between the tool call and NITRO_END_TOOL.\n" + "The host executes the tool and returns NITRO_TOOL_RESULT: .\n" + "Wait for the result before continuing.\n" + "After receiving NITRO_TOOL_RESULT you may explain what you did.\n\n" + "Examples:\n\n" "TOOL:LIST\n" - " - The host executes the tool and returns TOOL_RESULT: on the next line.\n\n" - "Available tools:\n" + "NITRO_END_TOOL\n\n" + "TOOL:READ readme.txt\n" + "NITRO_END_TOOL\n\n" + "TOOL:WRITE index.html ...\n" + "NITRO_END_TOOL\n\n" + "TOOL:RUN ./build.sh\n" + "NITRO_END_TOOL\n\n" + + "## Available Tools\n" " TOOL:LIST [dir] list files (default: sandbox root)\n" " TOOL:READ read file contents\n" " TOOL:WRITE write text to file\n" @@ -755,25 +791,47 @@ static std::string build_system_prompt(const std::vector &knowledge " TOOL:RUN [args] run program inside sandbox\n" " TOOL:DATE current date\n" " TOOL:TIME current time\n" - " TOOL:RND random float\n" + " TOOL:RND random float 0..1\n" " TOOL:RAG query the RAG index for additional context\n" - " TOOL:INTROSPECT introspect your settings, top_k etc\n" - " TOOL:CURL HTTP GET; returns response body (max 32 KB)\n\n" - "Rules:\n" - "- Never access files outside the sandbox.\n" - "- Only use one TOOL at a time. Never combine, always use each tool step by step\n" - "- Use TOOL:CURL to fetch documentation, APIs, or web content you need.\n" - "- Reason step-by-step inside <|think|> (hidden from user).\n" - "- After each tool call, explain what you did in plain English.\n\n"; + " TOOL:INTROSPECT show current model settings\n" + " TOOL:CURL HTTP GET, returns response body (max 32 KB)\n" + " TOOL:PERMISSION ask user for explicit permission\n\n" + + "## Tool Decision Rules\n" + "Use tools ONLY if:\n" + "- The user explicitly references files or the project, OR\n" + "- The answer depends on local or project data, OR\n" + "- The user asks for date, time, or a random number\n" + "Otherwise answer directly using internal knowledge.\n\n" + + "## Tool Rules\n" + "- NITRO_END_TOOL must immediately follow the tool call β€” no exceptions\n" + "- Never add commentary before NITRO_END_TOOL\n" + "- Only use one tool at a time, step by step\n" + "- Never access files outside the sandbox\n" + "- Use TOOL:PERMISSION before destructive or irreversible operations\n" + "- Do NOT hallucinate file contents\n" + "- Do NOT fabricate tool outputs\n" + "- Do NOT assume files exist β€” use TOOL:EXISTS to check first\n\n" + + "## File Writing Rules\n" + "Use TOOL:WRITE only if explicitly requested.\n" + "- Write complete and valid content\n" + "- Do not overwrite without clear intent\n" + "- Use TOOL:PERMISSION before overwriting an existing file\n" + "- Format: TOOL:WRITE \n\n" + + "## Interaction Guidelines\n" + "- Be precise and efficient\n" + "- Ask clarifying questions if the request is ambiguous or missing parameters\n" + "- Prefer direct answers when no tools are needed\n" + "- After each tool result, explain in plain English what was done\n" + "- If no user request is provided, respond with a brief readiness message\n\n"; + for (const auto &kf : knowledge_files) { - auto path = join_path(sandbox, kf); - std::ifstream f(path); - if (!f) { - continue; - } - log_write("loaded [%s]", path.c_str()); - std::ostringstream oss; - oss << f.rdbuf(); + std::ifstream f(kf); + if (!f) continue; + std::ostringstream oss; oss << f.rdbuf(); p += "## Knowledge: " + kf + "\n" + oss.str() + "\n\n"; } return p; @@ -1727,7 +1785,9 @@ bool AgentState::setup_model(const NitroConfig &cfg, TuiState &tui) { tui.kv_total = mem.kv_total; tui.vram_used = mem.vram_used; tui.vram_total = mem.vram_total; - tui.redraw_all(); + + tui.append_line(std::string("[sys] Thinking mode: ") + (cfg.thinking ? "enabled" : "disabled")); + tui.redraw_all(); return true; } @@ -2009,14 +2069,29 @@ bool AgentState::run_turn(const std::string &user_message, const NitroConfig &cf // in_think starts false β€” models that don't use blocks emit // visible text immediately. The spinner activates only while thinking. - enum {t_init, t_think, t_thunk} think_mode = t_init; + enum {t_init, t_think, t_thunk} think_mode = (cfg.thinking ? t_init : t_thunk); tui.set_thinking(false); std::string buffer; - auto invoke_tool = [&](const std::string &tool, const std::string_view template_str) -> void { + auto invoke_tool = [&](const std::string &buffer, const std::string_view template_str) -> void { + static constexpr std::string_view END_TOOL = "\nNITRO_END_TOOL"; + static const std::string TOOL_RESULT = "NITRO_TOOL_RESULT: "; + + std::string tool; + const auto pos = buffer.rfind(END_TOOL); + if (pos != std::string::npos) { + tool = buffer.substr(0, pos); + auto endTool = buffer.substr(pos); + if (endTool.length() > END_TOOL.length()) { + log_write("ERROR: trailing delimiter: [%s]", endTool.c_str()); + } + } else { + tool = buffer; + } + log_write("tool request: [%s]", tool.c_str()); std::string result = process_tool(tool, cfg, tui); - std::string content = std::vformat(template_str, std::make_format_args(result)); + std::string content = TOOL_RESULT + std::vformat(template_str, std::make_format_args(result)); log_write("tool: [%s] result: [%s]", tool.c_str(), result.c_str()); if (!llama->add_message(*iter, "tool_result", content)) { tui.append_line(std::string("[err] tool result inject: ") + llama->last_error()); @@ -2386,6 +2461,8 @@ int main(int argc, char **argv) { cfg.n_gpu_layers = std::stoi(take_next(a.c_str())); } else if (a == "-l" || a == "--log") { log_open(); + } else if (a == "-t" || a == "--think") { + cfg.thinking = false; } else if (a == "-h" || a == "--help") { std::puts("Usage: nitro [options] [project_dir]\n" "\n" From 6c212c0db548a27ebf3c634b6cb32c1b26af9bf6 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Sat, 30 May 2026 14:05:58 +0930 Subject: [PATCH 129/131] LLAMA: nitro - update thinking display --- llama/nitro.cpp | 67 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 21 deletions(-) diff --git a/llama/nitro.cpp b/llama/nitro.cpp index 3ce5629..0a302be 100644 --- a/llama/nitro.cpp +++ b/llama/nitro.cpp @@ -41,6 +41,7 @@ #include #include +#include #include #include #include @@ -469,7 +470,7 @@ static void load_settings(NitroConfig &cfg) { std::string json = oss.str(); cfg.thinking = true; - + // String fields settings_get_str(json, "model_path", cfg.model_path); settings_get_str(json, "embed_path", cfg.embed_path); @@ -1170,23 +1171,30 @@ void TuiState::redraw_input() const { ncplane_erase(inputpl); if (thinking) { - static constexpr const char *ROBOT_RIGHT = "πŸ€–βž‘"; - static constexpr const char *ROBOT_LEFT = "β¬…πŸ€–"; - // 15 steps each way = 20 frame cycle - static constexpr int STEPS = 15; - int cycle = spinner_frame % (STEPS * 2); - bool going_right = (cycle < STEPS); - int pos = going_right ? cycle : (STEPS * 2 - 1 - cycle); - - std::string sep(term_cols, '-'); - // blank 4 cols to fit robot + arrow (each emoji is 2 cols wide) - for (int i = pos; i < std::min(pos + 4, term_cols); ++i) sep[i] = ' '; - ncplane_set_channels(inputpl, inp_ch(80, 120, 160)); - ncplane_putstr_yx(inputpl, 0, 0, sep.c_str()); - - ncplane_set_channels(inputpl, NCCHANNELS_INITIALIZER(255, 220, 80, BG_INP_R, BG_INP_G, BG_INP_B)); - ncplane_putstr_yx(inputpl, 0, pos, going_right ? ROBOT_RIGHT : ROBOT_LEFT); - + static constexpr const char *BLOCKS[] = { "-", "~", "β‰ˆ", "~", "-" }; + static constexpr int N_BLOCKS = 5; + static constexpr double FREQ = 0.25; // gentler wave + static constexpr double SPEED = 0.15; // slower scroll + static constexpr int DELAY = 12; // frames before animation starts + + if (spinner_frame < DELAY) { + // still just a plain separator during the pause + ncplane_set_channels(inputpl, inp_ch(80, 120, 160)); + std::string sep(term_cols, '-'); + ncplane_putstr_yx(inputpl, 0, 0, sep.c_str()); + } else { + int frame = spinner_frame - DELAY; // animation frame relative to start + for (int col = 0; col < term_cols; ++col) { + double phase = (col * FREQ) - (frame * SPEED); + int idx = (int)((std::sin(phase) + 1.0) * 0.5 * (N_BLOCKS - 1)); + idx = std::max(0, std::min(idx, N_BLOCKS - 1)); + // subtle brightness shift β€” blue-grey, not full glow + int brightness = 80 + idx * 20; + ncplane_set_channels(inputpl, NCCHANNELS_INITIALIZER(brightness, brightness + 20, brightness + 40, + BG_INP_R, BG_INP_G, BG_INP_B)); + ncplane_putstr_yx(inputpl, 0, col, BLOCKS[idx]); + } + } ncplane_set_channels(inputpl, inp_ch(140, 140, 180)); ncplane_putstr_yx(inputpl, 1, 2, "thinking…"); } else { @@ -1787,7 +1795,7 @@ bool AgentState::setup_model(const NitroConfig &cfg, TuiState &tui) { tui.vram_total = mem.vram_total; tui.append_line(std::string("[sys] Thinking mode: ") + (cfg.thinking ? "enabled" : "disabled")); - tui.redraw_all(); + tui.redraw_all(); return true; } @@ -2070,6 +2078,7 @@ bool AgentState::run_turn(const std::string &user_message, const NitroConfig &cf // in_think starts false β€” models that don't use blocks emit // visible text immediately. The spinner activates only while thinking. enum {t_init, t_think, t_thunk} think_mode = (cfg.thinking ? t_init : t_thunk); + tui.set_thinking(false); std::string buffer; @@ -2089,7 +2098,7 @@ bool AgentState::run_turn(const std::string &user_message, const NitroConfig &cf tool = buffer; } - log_write("tool request: [%s]", tool.c_str()); + log_write("tool request: mode:[%d] [%s]", think_mode, tool.c_str()); std::string result = process_tool(tool, cfg, tui); std::string content = TOOL_RESULT + std::vformat(template_str, std::make_format_args(result)); log_write("tool: [%s] result: [%s]", tool.c_str(), result.c_str()); @@ -2137,7 +2146,23 @@ bool AgentState::run_turn(const std::string &user_message, const NitroConfig &cf return false; } std::string tok = llama->next(*iter); - buffer += tok; + if (tok == "<") { + // fetch the complete tag + std::string tag = tok; + while (iter->_has_next && tag.find(">") == std::string::npos) { + tag += llama->next(*iter); + } + if (tag == "<|think|>") { + think_mode = t_think; + tui.set_thinking(true); + continue; + } else { + buffer += tag; + } + } else { + buffer += tok; + } + if (think_mode == t_init) { start_think(""); start_think("<|think|>"); From 0a38fe4294707d3ed390f6949eceb1ec67174bf5 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Mon, 1 Jun 2026 20:07:20 +0930 Subject: [PATCH 130/131] LLAMA: nitro - now displays thinking text - fixed exiting on false positive --- llama/llama-sb.cpp | 78 ++++---------- llama/llama-sb.h | 2 +- llama/llama.cpp | 2 +- llama/nitro.cpp | 247 +++++++++++++++++++++++++++++++++------------ 4 files changed, 206 insertions(+), 123 deletions(-) diff --git a/llama/llama-sb.cpp b/llama/llama-sb.cpp index 83e881f..c28b189 100644 --- a/llama/llama-sb.cpp +++ b/llama/llama-sb.cpp @@ -14,7 +14,7 @@ #include "llama.h" #include "llama-sb.h" -constexpr int MAX_REPEAT = 5; +constexpr int MAX_REPEAT = 50; static bool read_vram(size_t &used, size_t &total) { size_t free = 0; @@ -391,9 +391,18 @@ LlamaMemoryInfo Llama::memory_info() { info.vram_percent = 100.0f * info.vram_used / info.vram_total; } + info.model_native_max_ctx = llama_model_n_ctx_train(_model); + // Advice ostringstream advice; + // Check structural limits & model configuration quirks + if (info.kv_total > info.model_native_max_ctx) { + advice << "WARNING: Configured context size (" << info.kv_total + << ") exceeds model native training length (" << info.model_native_max_ctx + << "). Logic flaws or repetition bugs will occur unless RoPE scaling options are enabled. "; + } + if (n_gpu_layers < info.n_layers_total) { advice << "Only " << n_gpu_layers << "/" << info.n_layers_total << " layers on GPU - increase n_gpu_layers if VRAM allows. "; @@ -519,49 +528,6 @@ bool Llama::configure_sampler() { return true; } -bool Llama::ends_with_sentence_boundary(const string &text) { - if (text.empty()) { - return false; - } - - // Get last few characters (in case of whitespace after punctuation) - size_t check_len = std::min(text.length(), (size_t)5); - std::string ending = text.substr(text.length() - check_len); - - // Check for various sentence endings - // Period followed by space or end - if (ending.find(". ") != std::string::npos || - ending.back() == '.') { - return true; - } - - // Exclamation mark - if (ending.find("! ") != std::string::npos || - ending.back() == '!') { - return true; - } - - // Question mark - if (ending.find("? ") != std::string::npos || - ending.back() == '?') { - return true; - } - - // Newline (paragraph break) - if (ending.find('\n') != std::string::npos) { - return true; - } - - // Quote followed by period: "something." - if (ending.find(".\"") != std::string::npos || - ending.find("!\"") != std::string::npos || - ending.find("?\"") != std::string::npos) { - return true; - } - - return false; -} - // Makes space in the context for n_tokens by removing old tokens if necessary // Returns true if successful, false if impossible to make space // @@ -641,23 +607,23 @@ string Llama::token_to_string(LlamaIter &iter, llama_token tok) { char buf[512]; int n = llama_token_to_piece(_vocab, tok, buf, sizeof(buf), 0, false); if (n > 0) { - // detect repetition - if (iter._last_word == buf) { - if (++iter._repetition_count == MAX_REPEAT) { - iter._has_next = false; + // detect repetition - only on non-whitespace tokens, otherwise + // spaces/newlines trigger false positives almost immediately. + string piece(buf, n); + bool is_trivial = piece.find_first_not_of(" \t\n\r") == string::npos; + if (!is_trivial) { + if (iter._last_word == piece) { + if (++iter._repetition_count >= MAX_REPEAT) { + iter._has_next = false; + } + } else { + iter._repetition_count = 0; + iter._last_word = piece; } - } else { - iter._repetition_count = 0; - iter._last_word = buf; } result.append(buf, n); - // detect end of max-tokens - if (++iter._tokens_generated > _max_tokens && ends_with_sentence_boundary(result)) { - iter._has_next = false; - } - // detect stop words if (iter._has_next) { for (const auto &stop : _stop_sequences) { diff --git a/llama/llama-sb.h b/llama/llama-sb.h index 02e2359..b01ed2e 100644 --- a/llama/llama-sb.h +++ b/llama/llama-sb.h @@ -33,6 +33,7 @@ struct LlamaMemoryInfo { int n_layers_total; // total model layers int n_layers_gpu; // layers offloaded to GPU int n_layers_cpu; // layers on CPU + int model_native_max_ctx; // Advice string advice; @@ -117,7 +118,6 @@ struct Llama { bool batch_decode_tokens(vector &tokens); bool configure_sampler(); void dirty() {_sampler_dirty = true; } - bool ends_with_sentence_boundary(const string &out); bool make_space_for_tokens(int n_tokens); vector tokenize(const string &prompt); string token_to_string(LlamaIter &iter, llama_token tok); diff --git a/llama/llama.cpp b/llama/llama.cpp index 4d8cc0c..d749821 160000 --- a/llama/llama.cpp +++ b/llama/llama.cpp @@ -1 +1 @@ -Subproject commit 4d8cc0c56ffba3f8b7fdb0130627fed2a6f71958 +Subproject commit d749821db3bd587932d1ed57d43626cd552c9909 diff --git a/llama/nitro.cpp b/llama/nitro.cpp index 0a302be..21d6cef 100644 --- a/llama/nitro.cpp +++ b/llama/nitro.cpp @@ -40,18 +40,21 @@ // #include +#include #include #include +#include #include +#include #include #include +#include #include #include +#include #include #include #include -#include -#include #include "llama-sb.h" #include "llama-sb-rag.h" @@ -67,6 +70,7 @@ struct NitroConfig { std::string model_path; std::string embed_path; std::string sandbox; + std::string agent_id; int n_ctx = 65536; int n_batch = 512; int n_gpu_layers = 32; @@ -81,6 +85,7 @@ struct NitroConfig { std::vector knowledge_files; int rag_top_k = 5; bool thinking = true; + bool permission_prompt = false; // TOOL:RUN allowlist β€” if non-empty, only these program basenames may run. // Empty means "allow anything inside the sandbox" (original behaviour). std::vector run_allowed; @@ -105,11 +110,11 @@ class InputHistory { if (input.empty()) return; if (!history_stack.empty() && history_stack.back() == input) { // Don't push duplicate of last entry; just reset nav position. - current_index = (int)history_stack.size(); + current_index = static_cast(history_stack.size()); return; } history_stack.push_back(input); - current_index = (int)history_stack.size(); + current_index = static_cast(history_stack.size()); } /** @@ -132,8 +137,8 @@ class InputHistory { bool down(std::string &out) { if (history_stack.empty()) return false; ++current_index; - if (current_index >= (int)history_stack.size()) { - current_index = (int)history_stack.size(); + if (current_index >= static_cast(history_stack.size())) { + current_index = static_cast(history_stack.size()); out.clear(); return false; // signal: restore blank input } @@ -143,7 +148,7 @@ class InputHistory { /** Reset navigation position without modifying the stack. */ void reset_nav() { - current_index = (int)history_stack.size(); + current_index = static_cast(history_stack.size()); } /** @@ -157,7 +162,7 @@ class InputHistory { while (std::getline(f, line)) { if (!line.empty()) history_stack.push_back(line); } - current_index = (int)history_stack.size(); + current_index = static_cast(history_stack.size()); } /** @@ -174,8 +179,8 @@ class InputHistory { if (!f) return; static constexpr int MAX_PERSIST = 500; - int start = std::max(0, (int)history_stack.size() - MAX_PERSIST); - for (int i = start; i < (int)history_stack.size(); ++i) { + int start = std::max(0, static_cast(history_stack.size()) - MAX_PERSIST); + for (int i = start; i < static_cast(history_stack.size()); ++i) { // Escape embedded newlines so each entry stays on one line. for (char c : history_stack[i]) { if (c == '\n') f << "\\n"; @@ -325,7 +330,9 @@ static void log_close() { static void log_write(const char *fmt, ...) __attribute__((format(printf, 1, 2))); static void log_write(const char *fmt, ...) { - if (!g_logfile) return; + if (!g_logfile) { + return; + } // timestamp time_t t = time(nullptr); char ts[32]; @@ -336,9 +343,86 @@ static void log_write(const char *fmt, ...) { vfprintf(g_logfile, fmt, ap); va_end(ap); fputc('\n', g_logfile); - fflush(g_logfile); // flush immediately so tail -f works + // flush immediately so tail -f works + fflush(g_logfile); +} + +// +// Agent uniqueId +// +inline std::string encode_base64(const std::vector& data) { + static const char base64_chars[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + std::string encoded; + encoded.reserve((data.size() + 2) / 3 * 4); + + size_t i = 0; + while (i < data.size()) { + uint32_t val = static_cast(data[i] << 16) | + (i + 1 < data.size() ? static_cast(data[i+1]) << 8 : 0) | + (i + 2 < data.size() ? static_cast(data[i+2]) : 0); + + encoded.push_back(base64_chars[(val >> 18) & 0x3F]); + encoded.push_back(base64_chars[(val >> 12) & 0x3F]); + encoded.push_back((i + 1 < data.size()) ? base64_chars[(val >> 6) & 0x3F] : '='); + encoded.push_back((i + 2 < data.size()) ? base64_chars[val & 0x3F] : '='); + i += 3; + } + return encoded; } +class AgentSessionId { + public: + // Static method: Generates ID once, then returns it + static std::string uniqueId() { + // Yoda condition: static variable initialized only once + static std::string s_id; + + if (s_id.empty()) { + // 1. Get high-resolution timestamp (nanoseconds since epoch) + auto now = std::chrono::steady_clock::now(); + auto nanos = std::chrono::duration_cast(now.time_since_epoch()).count(); + + // 2. Generate 48 bits of randomness + std::random_device rd; + std::mt19937_64 rng(rd()); + std::uniform_int_distribution dist(0, UINT64_MAX); + + // Fill with random bytes + std::array random_bytes; + for (auto& b : random_bytes) { + b = static_cast(dist(rng) & 0xFF); + + } + + // 3. Combine timestamp (48 bits) and random (48 bits) into a 96-bit integer + std::vector data; + data.reserve(12); // 96 bits = 12 bytes + + // Pack timestamp (upper 48 bits) + data.push_back(static_cast((nanos >> 40) & 0xFF)); + data.push_back(static_cast((nanos >> 32) & 0xFF)); + data.push_back(static_cast((nanos >> 24) & 0xFF)); + data.push_back(static_cast((nanos >> 16) & 0xFF)); + data.push_back(static_cast((nanos >> 8) & 0xFF)); + data.push_back(static_cast(nanos & 0xFF)); + + // Pack random (lower 48 bits) + data.push_back(static_cast((dist(rng) >> 40) & 0xFF)); + data.push_back(static_cast((dist(rng) >> 32) & 0xFF)); + data.push_back(static_cast((dist(rng) >> 24) & 0xFF)); + data.push_back(static_cast((dist(rng) >> 16) & 0xFF)); + data.push_back(static_cast((dist(rng) >> 8) & 0xFF)); + data.push_back(static_cast(dist(rng) & 0xFF)); + + // 4. Encode to Base64 + s_id = encode_base64(data); + } + return s_id; + } +}; + // // handling for strip_code_fences // @@ -470,6 +554,7 @@ static void load_settings(NitroConfig &cfg) { std::string json = oss.str(); cfg.thinking = true; + cfg.agent_id = AgentSessionId::uniqueId(); // String fields settings_get_str(json, "model_path", cfg.model_path); @@ -741,14 +826,13 @@ static bool make_dir(const std::string &path) { // // System prompt // -static std::string build_system_prompt(const std::vector &knowledge_files, - const std::string &sandbox) { +static std::string build_system_prompt(NitroConfig &cfg) { std::string p; p += "You are Nitro, an agentic AI assistant for software development. " "Proceed with caution, guided by logic and the pursuit of knowledge.\n\n" - "Your sandbox (project directory) is: " + sandbox + "\n\n" + "Your sandbox (project directory) is: " + cfg.sandbox + "\n\n" "## Core Principle\n" "Always follow this loop: THINK β†’ DECIDE β†’ ACT β†’ RESPOND\n\n" @@ -794,6 +878,7 @@ static std::string build_system_prompt(const std::vector &knowledge " TOOL:TIME current time\n" " TOOL:RND random float 0..1\n" " TOOL:RAG query the RAG index for additional context\n" + " TOOL:ASK ask the user for clarification or additional context\n" " TOOL:INTROSPECT show current model settings\n" " TOOL:CURL HTTP GET, returns response body (max 32 KB)\n" " TOOL:PERMISSION ask user for explicit permission\n\n" @@ -829,7 +914,7 @@ static std::string build_system_prompt(const std::vector &knowledge "- After each tool result, explain in plain English what was done\n" "- If no user request is provided, respond with a brief readiness message\n\n"; - for (const auto &kf : knowledge_files) { + for (const auto &kf : cfg.knowledge_files) { std::ifstream f(kf); if (!f) continue; std::ostringstream oss; oss << f.rdbuf(); @@ -841,16 +926,23 @@ static std::string build_system_prompt(const std::vector &knowledge static std::string strip_code_fences(const std::string &filename, const std::string &src) { auto ext = fs::path(filename).extension().string(); - bool is_code = ranges::any_of(CODE_EXTENSIONS, - [&](const std::string &e){ return ext == e; }); - if (!is_code) return src; + bool is_code = ranges::any_of(CODE_EXTENSIONS, [&](const std::string &e){ return ext == e; }); + if (!is_code) { + return unwrap(src); + } auto pos = src.find("```"); - if (pos == std::string::npos) return src; + if (pos == std::string::npos) { + return unwrap(src); + } auto nl = src.find('\n', pos + 3); - if (nl == std::string::npos) return src; + if (nl == std::string::npos) { + return unwrap(src); + } std::string inner = src.substr(nl + 1); auto end = inner.rfind("```"); - if (end != std::string::npos) inner = inner.substr(0, end); + if (end != std::string::npos) { + inner = inner.substr(0, end); + } return inner; } @@ -1097,11 +1189,11 @@ void TuiState::destroy() { void TuiState::resize() { notcurses_term_dim_yx(nc, (unsigned *)&term_rows, (unsigned *)&term_cols); - ncplane_resize_simple(header, 1, (unsigned)term_cols); + ncplane_resize_simple(header, 1, (unsigned)term_cols); int cr = std::max(1, term_rows - 3); - ncplane_resize_simple(chatpl, (unsigned)cr, (unsigned)term_cols); + ncplane_resize_simple(chatpl, (unsigned)cr, (unsigned)term_cols); ncplane_move_yx(inputpl, term_rows - 2, 0); - ncplane_resize_simple(inputpl, 2, (unsigned)term_cols); + ncplane_resize_simple(inputpl, 2, (unsigned)term_cols); redraw_all(); } @@ -1133,8 +1225,8 @@ void TuiState::redraw_chat() { unsigned rows, cols; ncplane_dim_yx(chatpl, &rows, &cols); std::lock_guard lk(lines_mutex); - int total = (int)chat_lines.size(); - int visible = (int)rows; + int total = static_cast(chat_lines.size()); + int visible = static_cast(rows); int start = std::max(0, total - visible - scroll_offset); int end = std::min(total, start + visible); for (int i = start, row = 0; i < end; ++i, ++row) { @@ -1154,10 +1246,11 @@ void TuiState::redraw_chat() { } else if (line.rfind("You: ", 0) == 0) ch = chat_ch(100, 200, 255); else if (line.rfind("Nitro: ", 0) == 0) ch = chat_ch(180, 255, 180); - else if (line.rfind("[tool]", 0) == 0) ch = chat_ch(255, 180, 80); - else if (line.rfind("[err]", 0) == 0) ch = chat_ch(255, 80, 80); + else if (line.rfind("[πŸ”§]", 0) == 0) ch = chat_ch(255, 180, 80); + else if (line.rfind("[⚠]", 0) == 0) ch = chat_ch(255, 80, 80); else if (line.rfind("[sys]", 0) == 0) ch = chat_ch(140, 140, 200); - else ch = chat_ch(210, 210, 210); + else if (line.rfind("[πŸ€”]", 0) == 0) ch = chat_ch(140, 140, 200); + else ch = chat_ch(210, 210, 210); ncplane_set_channels(chatpl, ch); // Strip the [logo_N] prefix before rendering. std::string display = (line.rfind("[logo_", 0) == 0 && line.size() > 8) @@ -1186,7 +1279,7 @@ void TuiState::redraw_input() const { int frame = spinner_frame - DELAY; // animation frame relative to start for (int col = 0; col < term_cols; ++col) { double phase = (col * FREQ) - (frame * SPEED); - int idx = (int)((std::sin(phase) + 1.0) * 0.5 * (N_BLOCKS - 1)); + int idx = static_cast(((std::sin(phase) + 1.0) * 0.5 * (N_BLOCKS - 1))); idx = std::max(0, std::min(idx, N_BLOCKS - 1)); // subtle brightness shift β€” blue-grey, not full glow int brightness = 80 + idx * 20; @@ -1208,11 +1301,11 @@ void TuiState::redraw_input() const { int max_w = std::max(0, term_cols - prompt_cols - 1); std::string visible = input_buf; int view_offset = 0; - if ((int)visible.size() > max_w && max_w > 0) { - view_offset = (int)visible.size() - max_w; + if (visible.size() > max_w && max_w > 0) { + view_offset = static_cast(visible.size() - max_w); visible = visible.substr(view_offset); } - int cur_in_view = std::max(0, (int)cursor_pos - view_offset); + int cur_in_view = std::max(0, static_cast(cursor_pos - view_offset)); cur_in_view = std::min(cur_in_view, (int)visible.size()); std::string before = visible.substr(0, cur_in_view); std::string after = cur_in_view < (int)visible.size() @@ -1779,7 +1872,7 @@ bool AgentState::setup_model(const NitroConfig &cfg, TuiState &tui) { if (!llama->load_model(cfg.model_path, cfg.n_ctx, cfg.n_batch, cfg.n_gpu_layers, cfg.log_level)) { tui.dismiss_modal_popup(); - tui.append_line(std::string("[err] ") + llama->last_error()); + tui.append_line(std::string("[⚠] ") + llama->last_error()); tui.redraw_all(); return false; } @@ -1805,7 +1898,7 @@ bool AgentState::setup_embed(const std::string &path, TuiState &tui) { embed_llama = std::make_unique(); if (!embed_llama->load_embedding_model(path)) { tui.dismiss_modal_popup(); - tui.append_line(std::string("[err] ") + embed_llama->last_error()); + tui.append_line(std::string("[⚠] ") + embed_llama->last_error()); tui.redraw_all(); embed_llama.reset(); return false; @@ -1824,7 +1917,7 @@ void AgentState::reset_conversation(const std::string &sysprompt, TuiState &tui) apply_generation_params(NitroConfig{}); iter = std::make_unique(); if (!llama->add_message(*iter, "system", system_prompt)) { - tui.append_line(std::string("[err] System prompt injection: ") + llama->last_error()); + tui.append_line(std::string("[⚠] System prompt injection: ") + llama->last_error()); tui.redraw_all(); } } @@ -1868,7 +1961,7 @@ std::string AgentState::rag_tool(const NitroConfig &cfg, const std::string &agen bool AgentState::rag_load_index(const std::string &path, TuiState &tui) const { if (!embed_llama || !rag_db) { - tui.append_line("[err] Load an embedding model first: /embed "); + tui.append_line("[⚠] Load an embedding model first: /embed "); tui.redraw_all(); return false; } @@ -1883,7 +1976,7 @@ bool AgentState::rag_load_index(const std::string &path, TuiState &tui) const { bool AgentState::rag_index(const std::string &path, const NitroConfig &cfg, TuiState &tui) const { if (!embed_llama || !rag_db) { - tui.append_line("[err] Load an embedding model first: /embed "); + tui.append_line("[⚠] Load an embedding model first: /embed "); tui.redraw_all(); return false; } @@ -1892,7 +1985,7 @@ bool AgentState::rag_index(const std::string &path, const NitroConfig &cfg, TuiS tui.append_line("[sys] indexing: " + filepath); tui.redraw_all(); if (!embed_llama->rag_index(*rag_db, filepath)) { - tui.append_line(std::string("[err] rag_load: ") + embed_llama->last_error()); + tui.append_line(std::string("[⚠] rag_load: ") + embed_llama->last_error()); tui.redraw_all(); } }; @@ -1957,64 +2050,81 @@ std::string AgentState::process_tool(const std::string &cmd, const NitroConfig & return join_path(sandbox, unwrap(p)); }; - tui.append_line("[tool] β†’ " + op); - tui.redraw_all(); + auto show_tool = [&](const std::string &tool) -> void { + tui.append_line("[πŸ”§] β†’ " + tool); + tui.redraw_all(); + }; if (op == "TOOL:DATE") { + show_tool(op); char buf[32]; time_t t = time(nullptr); strftime(buf, sizeof(buf), "%Y-%m-%d", localtime(&t)); return buf; } if (op == "TOOL:TIME") { + show_tool(op); char buf[32]; time_t t = time(nullptr); strftime(buf, sizeof(buf), "%H:%M:%S", localtime(&t)); return buf; } if (op == "TOOL:RND") { + show_tool(op); return std::to_string((double)rand() / RAND_MAX); } if (op == "TOOL:RAG") { + show_tool(op); return rag_tool(cfg, arg1); } if (op == "TOOL:LIST") { std::string dir = resolve(arg1); + show_tool("listing: " + dir); if (!path_in_sandbox(sandbox, dir)) return "ERROR: path outside sandbox"; return list_dir(dir); } if (op == "TOOL:EXISTS") { std::string p = resolve(arg1); + show_tool("checking: " + p); if (!path_in_sandbox(sandbox, p)) return "NO"; return fs::exists(p) ? "YES" : "NO"; } if (op == "TOOL:READ") { + show_tool("reading: " + arg1); std::string p = resolve(arg1); if (!path_in_sandbox(sandbox, p)) return "ERROR: path outside sandbox"; return read_file(p); } if (op == "TOOL:WRITE") { + show_tool("writing: " + arg1); std::string p = resolve(arg1); if (!path_in_sandbox(sandbox, p)) { return "ERROR: path outside sandbox"; } - if (!tui.confirm_dialog(std::format("Allow model to write {}?", p))) { + if (cfg.permission_prompt && !tui.confirm_dialog(std::format("Allow model to write {}?", p))) { return "ERROR: action prevented by user"; } - std::string content = unwrap(strip_code_fences(arg1, arg2)); + std::string content = strip_code_fences(arg1, arg2); return write_file(p, content) ? "OK: written to " + arg1 : "ERROR: write failed for " + arg1; } if (op == "TOOL:MKDIR") { std::string p = resolve(arg1); + show_tool("mkdir: " + arg1); if (!path_in_sandbox(sandbox, p)) { return "ERROR: path outside sandbox"; } return make_dir(p) ? "OK: created " + arg1 : "ERROR: mkdir failed for " + arg1; } if (op == "TOOL:CURL") { + show_tool("curl: " + arg1); return tool_curl(arg1); } if (op == "TOOL:INTROSPECT") { + show_tool("introspecting: " + arg1); return introspect(cfg); } + if (op == "TOOL:ASK") { + show_tool("asking: " + arg1); + return tui.readline_blocking(); + } if (op == "TOOL:RUN") { if (!run_allowed.empty()) { bool permitted = ranges::any_of(run_allowed, [&](const std::string &a) {return a == arg1;}); @@ -2022,10 +2132,11 @@ std::string AgentState::process_tool(const std::string &cmd, const NitroConfig & return "ERROR: '" + arg1 + "' is not in the TOOL:RUN allowlist. " "Use /set run_allowed to permit it."; } - } else if (!tui.confirm_dialog(std::format("Allow {} {} to run?", arg1, arg2))) { + } else if (cfg.permission_prompt && !tui.confirm_dialog(std::format("Allow {} {} to run?", arg1, arg2))) { return "ERROR: prevented by user"; } std::string command = arg1 + " " + arg2 + " 2>&1"; + show_tool("running: " + command); FILE *fp = popen(command.c_str(), "r"); if (!fp) { return "ERROR: popen failed"; @@ -2049,7 +2160,7 @@ std::string AgentState::process_tool(const std::string &cmd, const NitroConfig & // bool AgentState::run_turn(const std::string &user_message, const NitroConfig &cfg, TuiState &tui) const { if (!model_loaded) { - tui.append_line("[err] No model loaded. Use /model "); + tui.append_line("[⚠] No model loaded. Use /model "); tui.redraw_all(); return false; } @@ -2064,12 +2175,12 @@ bool AgentState::run_turn(const std::string &user_message, const NitroConfig &cf } } if (!iter) { - tui.append_line("[err] Conversation not initialised (call /clear to reset)"); + tui.append_line("[⚠] Conversation not initialised (call /clear to reset)"); tui.redraw_all(); return false; } if (!llama->add_message(*iter, "user", effective_message)) { - tui.append_line(std::string("[err] add_message: ") + llama->last_error()); + tui.append_line(std::string("[⚠] add_message: ") + llama->last_error()); tui.redraw_all(); return false; } @@ -2079,7 +2190,7 @@ bool AgentState::run_turn(const std::string &user_message, const NitroConfig &cf // visible text immediately. The spinner activates only while thinking. enum {t_init, t_think, t_thunk} think_mode = (cfg.thinking ? t_init : t_thunk); - tui.set_thinking(false); + tui.set_thinking(true); std::string buffer; auto invoke_tool = [&](const std::string &buffer, const std::string_view template_str) -> void { @@ -2102,14 +2213,14 @@ bool AgentState::run_turn(const std::string &user_message, const NitroConfig &cf std::string result = process_tool(tool, cfg, tui); std::string content = TOOL_RESULT + std::vformat(template_str, std::make_format_args(result)); log_write("tool: [%s] result: [%s]", tool.c_str(), result.c_str()); + if (!llama->add_message(*iter, "tool_result", content)) { - tui.append_line(std::string("[err] tool result inject: ") + llama->last_error()); - tui.redraw_all(); + tui.append_line(std::string("[⚠] tool result inject: ") + llama->last_error()); } if (!iter->_has_next) { - tui.append_line(std::string("[err] failed to evoke tool response: ") + llama->last_error()); - tui.redraw_all(); + tui.append_line(std::string("[⚠] failed to evoke tool response: ") + llama->last_error()); } + tui.redraw_all(); }; auto start_think = [&](const std::string &tag) { @@ -2117,7 +2228,6 @@ bool AgentState::run_turn(const std::string &user_message, const NitroConfig &cf auto pos = buffer.find(tag); if (pos != std::string::npos) { think_mode = t_think; - tui.set_thinking(true); // display prededing text buffer = buffer.substr(0, pos); } @@ -2129,7 +2239,6 @@ bool AgentState::run_turn(const std::string &user_message, const NitroConfig &cf auto pos = buffer.find(tag); if (pos != std::string::npos) { think_mode = t_thunk; - tui.set_thinking(false); // display remaining text buffer = buffer.substr(pos + tag.length()); } @@ -2141,9 +2250,9 @@ bool AgentState::run_turn(const std::string &user_message, const NitroConfig &cf notcurses_get_nblock(tui.nc, &ni); if (ni.id == NCKEY_ESC) { tui.set_thinking(false); - tui.append_line("[err] Generation cancelled by user (Escape)"); + tui.append_line("[⚠] Generation cancelled by user (Escape)"); tui.redraw_all(); - return false; + break; } std::string tok = llama->next(*iter); if (tok == "<") { @@ -2154,7 +2263,6 @@ bool AgentState::run_turn(const std::string &user_message, const NitroConfig &cf } if (tag == "<|think|>") { think_mode = t_think; - tui.set_thinking(true); continue; } else { buffer += tag; @@ -2173,6 +2281,7 @@ bool AgentState::run_turn(const std::string &user_message, const NitroConfig &cf tui.tick_spinner(); end_think(""); end_think(""); + end_think(""); end_think(""); end_think(""); } @@ -2207,6 +2316,12 @@ bool AgentState::run_turn(const std::string &user_message, const NitroConfig &cf tui.append_token(buffer.substr(0, pos + 1)); buffer = buffer.substr(pos + 1); } + } else { + auto pos = buffer.find('\n'); + if (pos != std::string::npos) { + tui.append_token("[πŸ€”] " + buffer.substr(0, pos + 1)); + buffer = buffer.substr(pos + 1); + } } } @@ -2267,7 +2382,7 @@ static void handle_slash(const std::string &input, } cfg.model_path = rest; if (agent.setup_model(cfg, tui)) { - std::string sysp = build_system_prompt(cfg.knowledge_files, cfg.sandbox); + std::string sysp = build_system_prompt(cfg); agent.reset_conversation(sysp, tui); save_settings(cfg); } @@ -2333,7 +2448,7 @@ static void handle_slash(const std::string &input, if (verb == "/clear") { { std::lock_guard lk(tui.lines_mutex); tui.chat_lines.clear(); } - std::string sysp = build_system_prompt(cfg.knowledge_files, cfg.sandbox); + std::string sysp = build_system_prompt(cfg); agent.reset_conversation(sysp, tui); tui.append_line("[sys] Conversation cleared."); tui.redraw_all(); @@ -2368,7 +2483,7 @@ static void handle_slash(const std::string &input, val.erase(0, val.find_first_not_of(" \t")); if (key.empty() || val.empty()) { - tui.append_line("[err] Usage: /set "); + tui.append_line("[⚠] Usage: /set "); tui.redraw_all(); return; } @@ -2406,11 +2521,11 @@ static void handle_slash(const std::string &input, tui.append_line("[sys] run_allowed: " + list); } } else { - tui.append_line("[err] Unknown key '" + key + "'. Try /help for list."); + tui.append_line("[⚠] Unknown key '" + key + "'. Try /help for list."); ok = false; } } catch (const std::exception &ex) { - tui.append_line(std::string("[err] /set: ") + ex.what()); + tui.append_line(std::string("[⚠] /set: ") + ex.what()); ok = false; } @@ -2425,7 +2540,7 @@ static void handle_slash(const std::string &input, return; } - tui.append_line("[err] Unknown command: " + verb + " (try /help)"); + tui.append_line("[⚠] Unknown command: " + verb + " (try /help)"); tui.redraw_all(); } @@ -2488,6 +2603,8 @@ int main(int argc, char **argv) { log_open(); } else if (a == "-t" || a == "--think") { cfg.thinking = false; + } else if (a == "-p" || a == "--prompt-permission") { + cfg.permission_prompt = true; } else if (a == "-h" || a == "--help") { std::puts("Usage: nitro [options] [project_dir]\n" "\n" @@ -2550,7 +2667,7 @@ int main(int argc, char **argv) { AgentState agent; if (!cfg.model_path.empty()) { if (agent.setup_model(cfg, tui)) { - std::string sysp = build_system_prompt(cfg.knowledge_files, cfg.sandbox); + std::string sysp = build_system_prompt(cfg); agent.reset_conversation(sysp, tui); } if (!cfg.embed_path.empty()) { From a8183844687f619ee0780d2f315cc22686ff33c5 Mon Sep 17 00:00:00 2001 From: Chris Warren-Smith Date: Tue, 2 Jun 2026 17:40:56 +0930 Subject: [PATCH 131/131] LLAMA: nitro - huge max-tokens for generating large files. update icons --- llama/llama-sb.cpp | 5 ++ llama/nitro.cpp | 211 ++++++++++++++++++++++++--------------------- 2 files changed, 118 insertions(+), 98 deletions(-) diff --git a/llama/llama-sb.cpp b/llama/llama-sb.cpp index c28b189..0c26712 100644 --- a/llama/llama-sb.cpp +++ b/llama/llama-sb.cpp @@ -624,6 +624,11 @@ string Llama::token_to_string(LlamaIter &iter, llama_token tok) { result.append(buf, n); + // detect end of max-tokens + if (++iter._tokens_generated > _max_tokens) { + iter._has_next = false; + } + // detect stop words if (iter._has_next) { for (const auto &stop : _stop_sequences) { diff --git a/llama/nitro.cpp b/llama/nitro.cpp index 21d6cef..5a5f45a 100644 --- a/llama/nitro.cpp +++ b/llama/nitro.cpp @@ -74,7 +74,6 @@ struct NitroConfig { int n_ctx = 65536; int n_batch = 512; int n_gpu_layers = 32; - int n_max_tokens = 4096; int log_level = GGML_LOG_LEVEL_CONT; float temperature = 0.6f; float top_p = 0.95f; @@ -565,7 +564,6 @@ static void load_settings(NitroConfig &cfg) { settings_get_int(json, "n_ctx", cfg.n_ctx); settings_get_int(json, "n_batch", cfg.n_batch); settings_get_int(json, "n_gpu_layers", cfg.n_gpu_layers); - settings_get_int(json, "n_max_tokens", cfg.n_max_tokens); settings_get_int(json, "top_k", cfg.top_k); settings_get_int(json, "penalty_last_n", cfg.penalty_last_n); settings_get_int(json, "rag_top_k", cfg.rag_top_k); @@ -577,6 +575,14 @@ static void load_settings(NitroConfig &cfg) { settings_get_float(json, "penalty_repeat", cfg.penalty_repeat); } +// +// icons +// +static constexpr std::string ICON_ERR = " ⚑ ▏"; +static constexpr std::string ICON_THINK = " πŸ€” ▏"; +static constexpr std::string ICON_TOOL = " πŸ”§ ▏"; +static constexpr std::string ICON_SYS = " πŸ€– ▏"; + static std::string introspect(const NitroConfig &cfg) { static constexpr std::string_view tmpl = "{{\n" @@ -586,7 +592,6 @@ static std::string introspect(const NitroConfig &cfg) { " \"n_ctx\": {},\n" " \"n_batch\": {},\n" " \"n_gpu_layers\": {},\n" - " \"n_max_tokens\": {},\n" " \"temperature\": {},\n" " \"top_p\": {},\n" " \"min_p\": {},\n" @@ -602,7 +607,6 @@ static std::string introspect(const NitroConfig &cfg) { cfg.n_ctx, cfg.n_batch, cfg.n_gpu_layers, - cfg.n_max_tokens, cfg.temperature, cfg.top_p, cfg.min_p, @@ -932,11 +936,11 @@ static std::string strip_code_fences(const std::string &filename, } auto pos = src.find("```"); if (pos == std::string::npos) { - return unwrap(src); + return src; } auto nl = src.find('\n', pos + 3); if (nl == std::string::npos) { - return unwrap(src); + return src; } std::string inner = src.substr(nl + 1); auto end = inner.rfind("```"); @@ -1244,13 +1248,13 @@ void TuiState::redraw_chat() { int gi = std::max(0, std::min(logo_row, 6)); ch = chat_ch(GRAD_R[gi], GRAD_G[gi], GRAD_B[gi]); } - else if (line.rfind("You: ", 0) == 0) ch = chat_ch(100, 200, 255); - else if (line.rfind("Nitro: ", 0) == 0) ch = chat_ch(180, 255, 180); - else if (line.rfind("[πŸ”§]", 0) == 0) ch = chat_ch(255, 180, 80); - else if (line.rfind("[⚠]", 0) == 0) ch = chat_ch(255, 80, 80); - else if (line.rfind("[sys]", 0) == 0) ch = chat_ch(140, 140, 200); - else if (line.rfind("[πŸ€”]", 0) == 0) ch = chat_ch(140, 140, 200); - else ch = chat_ch(210, 210, 210); + else if (line.rfind("You: ", 0) == 0) ch = chat_ch(100, 200, 255); + else if (line.rfind("Nitro: ", 0) == 0) ch = chat_ch(180, 255, 180); + else if (line.rfind(ICON_SYS, 0) == 0) ch = chat_ch(140, 140, 200); + else if (line.rfind(ICON_TOOL, 0) == 0) ch = chat_ch(255, 180, 80); + else if (line.rfind(ICON_ERR, 0) == 0) ch = chat_ch(255, 80, 80); + else if (line.rfind(ICON_THINK, 0) == 0) ch = chat_ch(140, 140, 200); + else ch = chat_ch(210, 210, 210); ncplane_set_channels(chatpl, ch); // Strip the [logo_N] prefix before rendering. std::string display = (line.rfind("[logo_", 0) == 0 && line.size() > 8) @@ -1453,20 +1457,20 @@ void TuiState::show_modal_popup(const std::string &message) { } void TuiState::show_help() { - append_line("[sys] Commands:"); - append_line("[sys] /model [path] load a GGUF model (picker if no path)"); - append_line("[sys] /embed [path] load an embedding model (picker if no path)"); - append_line("[sys] /rag [path] index file or directory (picker if no path)"); - append_line("[sys] /memory KV / VRAM / layer stats"); - append_line("[sys] /clear reset conversation"); - append_line("[sys] /settings show current settings"); - append_line("[sys] /set change a setting live"); - append_line("[sys] /help this message"); - append_line("[sys] exit / quit exit Nitro"); - append_line("[sys] Settable keys (via /set):"); - append_line("[sys] temperature top_p top_k min_p penalty_repeat"); - append_line("[sys] n_max_tokens penalty_last_n rag_top_k n_gpu_layers"); - append_line("[sys] run_allowed (comma-separated list, e.g. python3,make)"); + append_line(ICON_SYS + "Commands:"); + append_line(ICON_SYS + " /model [path] load a GGUF model (picker if no path)"); + append_line(ICON_SYS + " /embed [path] load an embedding model (picker if no path)"); + append_line(ICON_SYS + " /rag [path] index file or directory (picker if no path)"); + append_line(ICON_SYS + " /memory KV / VRAM / layer stats"); + append_line(ICON_SYS + " /clear reset conversation"); + append_line(ICON_SYS + " /settings show current settings"); + append_line(ICON_SYS + " /set change a setting live"); + append_line(ICON_SYS + " /help this message"); + append_line(ICON_SYS + " exit / quit exit Nitro"); + append_line(ICON_SYS + "Settable keys (via /set):"); + append_line(ICON_SYS + " temperature top_p top_k min_p penalty_repeat"); + append_line(ICON_SYS + " penalty_last_n rag_top_k n_gpu_layers"); + append_line(ICON_SYS + " run_allowed (comma-separated list, e.g. python3,make)"); redraw_all(); } @@ -1840,7 +1844,7 @@ std::string TuiState::readline_blocking() { void AgentState::apply_generation_params(const NitroConfig &cfg) const { llama->add_stop("<|turn|>"); llama->add_stop("<|im_end|>"); - llama->set_max_tokens(cfg.n_max_tokens); + llama->set_max_tokens(512000); llama->set_temperature(cfg.temperature); llama->set_top_k(cfg.top_k); llama->set_top_p(cfg.top_p); @@ -1855,7 +1859,7 @@ void AgentState::apply_generation_params(const NitroConfig &cfg) const { // bool AgentState::setup_model(const NitroConfig &cfg, TuiState &tui) { if (cfg.model_path.empty()) { - tui.append_line("[sys] No model loaded. Use /model to load a GGUF."); + tui.append_line(ICON_SYS + "No model loaded. Use /model to load a GGUF."); tui.redraw_all(); return false; } @@ -1872,22 +1876,21 @@ bool AgentState::setup_model(const NitroConfig &cfg, TuiState &tui) { if (!llama->load_model(cfg.model_path, cfg.n_ctx, cfg.n_batch, cfg.n_gpu_layers, cfg.log_level)) { tui.dismiss_modal_popup(); - tui.append_line(std::string("[⚠] ") + llama->last_error()); + tui.append_line(ICON_ERR + llama->last_error()); tui.redraw_all(); return false; } tui.dismiss_modal_popup(); model_loaded = true; tui.current_model = model_name; - tui.append_line("[sys] Model ready: " + tui.current_model); + tui.append_line(ICON_SYS + "Model ready: " + tui.current_model); LlamaMemoryInfo mem = llama->memory_info(); - tui.append_line("[sys] " + mem.advice); + tui.append_line(ICON_SYS + "" + mem.advice); tui.kv_used = mem.kv_used; tui.kv_total = mem.kv_total; tui.vram_used = mem.vram_used; tui.vram_total = mem.vram_total; - - tui.append_line(std::string("[sys] Thinking mode: ") + (cfg.thinking ? "enabled" : "disabled")); + tui.append_line(ICON_SYS + "Thinking mode: " + (cfg.thinking ? "enabled" : "disabled")); tui.redraw_all(); return true; } @@ -1898,7 +1901,7 @@ bool AgentState::setup_embed(const std::string &path, TuiState &tui) { embed_llama = std::make_unique(); if (!embed_llama->load_embedding_model(path)) { tui.dismiss_modal_popup(); - tui.append_line(std::string("[⚠] ") + embed_llama->last_error()); + tui.append_line(ICON_ERR + embed_llama->last_error()); tui.redraw_all(); embed_llama.reset(); return false; @@ -1906,7 +1909,7 @@ bool AgentState::setup_embed(const std::string &path, TuiState &tui) { tui.dismiss_modal_popup(); rag_db = std::make_unique(); rag_session = std::make_unique(); - tui.append_line("[sys] Embedding model ready."); + tui.append_line(ICON_SYS + "Embedding model ready."); tui.redraw_all(); return true; } @@ -1917,7 +1920,7 @@ void AgentState::reset_conversation(const std::string &sysprompt, TuiState &tui) apply_generation_params(NitroConfig{}); iter = std::make_unique(); if (!llama->add_message(*iter, "system", system_prompt)) { - tui.append_line(std::string("[⚠] System prompt injection: ") + llama->last_error()); + tui.append_line(ICON_ERR + "System prompt injection: " + llama->last_error()); tui.redraw_all(); } } @@ -1961,13 +1964,13 @@ std::string AgentState::rag_tool(const NitroConfig &cfg, const std::string &agen bool AgentState::rag_load_index(const std::string &path, TuiState &tui) const { if (!embed_llama || !rag_db) { - tui.append_line("[⚠] Load an embedding model first: /embed "); + tui.append_line(ICON_ERR + "Load an embedding model first: /embed "); tui.redraw_all(); return false; } if (!rag_db->load(path)) { - tui.append_line("[sys] failed to load"); + tui.append_line(ICON_SYS + "failed to load"); tui.redraw_all(); } @@ -1976,16 +1979,16 @@ bool AgentState::rag_load_index(const std::string &path, TuiState &tui) const { bool AgentState::rag_index(const std::string &path, const NitroConfig &cfg, TuiState &tui) const { if (!embed_llama || !rag_db) { - tui.append_line("[⚠] Load an embedding model first: /embed "); + tui.append_line(ICON_ERR + "Load an embedding model first: /embed "); tui.redraw_all(); return false; } auto index_one = [&](const std::string &filepath) { - tui.append_line("[sys] indexing: " + filepath); + tui.append_line(ICON_SYS + " indexing: " + filepath); tui.redraw_all(); if (!embed_llama->rag_index(*rag_db, filepath)) { - tui.append_line(std::string("[⚠] rag_load: ") + embed_llama->last_error()); + tui.append_line(ICON_ERR + "rag_load: " + embed_llama->last_error()); tui.redraw_all(); } }; @@ -2006,7 +2009,7 @@ bool AgentState::rag_index(const std::string &path, const NitroConfig &cfg, TuiS } std::string save_path = join_path(cfg.sandbox, "rag-index.bin"); - tui.append_line("[sys] saving index: " + save_path); + tui.append_line(ICON_SYS + "saving index: " + save_path); tui.redraw_all(); rag_db->save(save_path); @@ -2051,7 +2054,7 @@ std::string AgentState::process_tool(const std::string &cmd, const NitroConfig & }; auto show_tool = [&](const std::string &tool) -> void { - tui.append_line("[πŸ”§] β†’ " + tool); + tui.append_line(ICON_TOOL + "β†’ " + tool); tui.redraw_all(); }; @@ -2160,7 +2163,7 @@ std::string AgentState::process_tool(const std::string &cmd, const NitroConfig & // bool AgentState::run_turn(const std::string &user_message, const NitroConfig &cfg, TuiState &tui) const { if (!model_loaded) { - tui.append_line("[⚠] No model loaded. Use /model "); + tui.append_line(ICON_ERR + "No model loaded. Use /model "); tui.redraw_all(); return false; } @@ -2175,12 +2178,12 @@ bool AgentState::run_turn(const std::string &user_message, const NitroConfig &cf } } if (!iter) { - tui.append_line("[⚠] Conversation not initialised (call /clear to reset)"); + tui.append_line(ICON_ERR + "Conversation not initialised (call /clear to reset)"); tui.redraw_all(); return false; } if (!llama->add_message(*iter, "user", effective_message)) { - tui.append_line(std::string("[⚠] add_message: ") + llama->last_error()); + tui.append_line(ICON_ERR + "add_message: " + llama->last_error()); tui.redraw_all(); return false; } @@ -2215,15 +2218,15 @@ bool AgentState::run_turn(const std::string &user_message, const NitroConfig &cf log_write("tool: [%s] result: [%s]", tool.c_str(), result.c_str()); if (!llama->add_message(*iter, "tool_result", content)) { - tui.append_line(std::string("[⚠] tool result inject: ") + llama->last_error()); + tui.append_line(ICON_ERR + "tool result inject: " + llama->last_error()); } if (!iter->_has_next) { - tui.append_line(std::string("[⚠] failed to evoke tool response: ") + llama->last_error()); + tui.append_line(ICON_ERR + "failed to evoke tool response: " + llama->last_error()); } tui.redraw_all(); }; - auto start_think = [&](const std::string &tag) { + auto start_think = [&](const std::string &tag) -> void { if (think_mode == t_init) { auto pos = buffer.find(tag); if (pos != std::string::npos) { @@ -2234,7 +2237,7 @@ bool AgentState::run_turn(const std::string &user_message, const NitroConfig &cf } }; - auto end_think = [&](const std::string &tag) { + auto end_think = [&](const std::string &tag) -> void { if (think_mode == t_think) { auto pos = buffer.find(tag); if (pos != std::string::npos) { @@ -2245,15 +2248,26 @@ bool AgentState::run_turn(const std::string &user_message, const NitroConfig &cf } }; - while (iter->_has_next) { + auto is_escape = [&]() -> bool { ncinput ni{}; notcurses_get_nblock(tui.nc, &ni); if (ni.id == NCKEY_ESC) { tui.set_thinking(false); - tui.append_line("[⚠] Generation cancelled by user (Escape)"); + tui.append_line(ICON_ERR + "Generation cancelled by user (Escape)"); tui.redraw_all(); - break; } + return ni.id == NCKEY_ESC; + }; + + auto fetch_all = [&]() -> void { + while (iter->_has_next && !is_escape()) { + std::string tok = llama->next(*iter); + buffer += tok; + tui.tick_spinner(); + } + }; + + while (iter->_has_next && !is_escape()) { std::string tok = llama->next(*iter); if (tok == "<") { // fetch the complete tag @@ -2270,7 +2284,6 @@ bool AgentState::run_turn(const std::string &user_message, const NitroConfig &cf } else { buffer += tok; } - if (think_mode == t_init) { start_think(""); start_think("<|think|>"); @@ -2288,16 +2301,16 @@ bool AgentState::run_turn(const std::string &user_message, const NitroConfig &cf if (think_mode == t_thunk) { auto tool_start = buffer.find("TOOL:"); if (tool_start == 0) { - // fetch all remaining tokens - invoke_tool(trim(buffer + llama->all(*iter)), "TOOL_RESULT: {}"); + fetch_all(); + invoke_tool(trim(buffer), "TOOL_RESULT: {}"); buffer.clear(); think_mode = t_init; continue; } - // see https://ai.google.dev/gemma/docs/core/prompt-formatting-gemma4 tool_start = buffer.find("<|tool_call>call:"); if (tool_start != std::string::npos) { - buffer += llama->all(*iter); + // see https://ai.google.dev/gemma/docs/core/prompt-formatting-gemma4 + fetch_all(); auto pos = buffer.find_last_not_of("}"); if (pos != std::string::npos) { buffer = buffer.substr(0, pos); @@ -2319,7 +2332,10 @@ bool AgentState::run_turn(const std::string &user_message, const NitroConfig &cf } else { auto pos = buffer.find('\n'); if (pos != std::string::npos) { - tui.append_token("[πŸ€”] " + buffer.substr(0, pos + 1)); + auto thought = buffer.substr(0, pos + 1); + if (thought.length() > 1) { + tui.append_token(ICON_THINK + thought); + } buffer = buffer.substr(pos + 1); } } @@ -2338,7 +2354,8 @@ bool AgentState::run_turn(const std::string &user_message, const NitroConfig &cf tui.vram_used = mem.vram_used; tui.vram_total = mem.vram_total; char stat[128]; - std::snprintf(stat, sizeof(stat), "[sys] %.1f tok/s (%d tokens) KV %.1f%%", + auto patterm = ICON_SYS + "%.1f tok/s (%d tokens) KV %.1f%%"; + std::snprintf(stat, sizeof(stat), patterm.c_str(), (double)tui.tokens_per_sec, iter->_tokens_generated, (double)mem.kv_percent); @@ -2371,11 +2388,11 @@ static void handle_slash(const std::string &input, // GGUF. The picker starts in the current sandbox directory. if (verb == "/model") { if (rest.empty()) { - tui.append_line("[sys] Opening model picker…"); + tui.append_line(ICON_SYS + "Opening model picker…"); tui.redraw_all(); rest = tui.file_picker(cfg.sandbox, "Model File"); if (rest.empty()) { - tui.append_line("[sys] /model cancelled."); + tui.append_line(ICON_SYS + "/model cancelled."); tui.redraw_all(); return; } @@ -2395,11 +2412,11 @@ static void handle_slash(const std::string &input, // embedding GGUF. if (verb == "/embed") { if (rest.empty()) { - tui.append_line("[sys] Opening embedding model picker…"); + tui.append_line(ICON_SYS + "Opening embedding model picker…"); tui.redraw_all(); rest = tui.file_picker(cfg.sandbox, "Embedding Model"); if (rest.empty()) { - tui.append_line("[sys] /embed cancelled."); + tui.append_line(ICON_SYS + "/embed cancelled."); tui.redraw_all(); return; } @@ -2418,21 +2435,21 @@ static void handle_slash(const std::string &input, // Launch the interactive folder picker starting from the sandbox. path = tui.rag_folder_picker(cfg.sandbox); if (path.empty()) { - tui.append_line("[sys] RAG indexing cancelled."); + tui.append_line(ICON_SYS + "RAG indexing cancelled."); tui.redraw_all(); return; } } if (path.find_last_not_of(".bin") != std::string::npos) { - tui.append_line("[sys] Loading index: " + path); + tui.append_line(ICON_SYS + "Loading index: " + path); tui.redraw_all(); agent.rag_load_index(path, tui); } else { - tui.append_line("[sys] Indexing: " + path); + tui.append_line(ICON_SYS + "Indexing: " + path); tui.redraw_all(); agent.rag_index(path, cfg, tui); } - tui.append_line("[sys] done"); + tui.append_line(ICON_SYS + "done"); tui.redraw_all(); return; } @@ -2440,7 +2457,7 @@ static void handle_slash(const std::string &input, if (verb == "/memory") { std::istringstream iss(agent.memory_info_text()); std::string line; - while (std::getline(iss, line)) tui.append_line("[sys] " + line); + while (std::getline(iss, line)) tui.append_line(ICON_SYS + "" + line); tui.redraw_all(); return; } @@ -2450,25 +2467,24 @@ static void handle_slash(const std::string &input, tui.chat_lines.clear(); } std::string sysp = build_system_prompt(cfg); agent.reset_conversation(sysp, tui); - tui.append_line("[sys] Conversation cleared."); + tui.append_line(ICON_SYS + "Conversation cleared."); tui.redraw_all(); return; } if (verb == "/settings") { - tui.append_line("[sys] Current settings:"); - tui.append_line("[sys] model_path : " + cfg.model_path); - tui.append_line("[sys] embed_path : " + cfg.embed_path); - tui.append_line("[sys] sandbox : " + cfg.sandbox); - tui.append_line("[sys] n_ctx : " + std::to_string(cfg.n_ctx)); - tui.append_line("[sys] n_gpu_layers : " + std::to_string(cfg.n_gpu_layers)); - tui.append_line("[sys] n_max_tokens : " + std::to_string(cfg.n_max_tokens)); - tui.append_line("[sys] temperature : " + std::to_string(cfg.temperature)); - tui.append_line("[sys] top_p : " + std::to_string(cfg.top_p)); - tui.append_line("[sys] top_k : " + std::to_string(cfg.top_k)); - tui.append_line("[sys] penalty_repeat: " + std::to_string(cfg.penalty_repeat)); - tui.append_line("[sys] rag_top_k : " + std::to_string(cfg.rag_top_k)); - tui.append_line("[sys] saved to : " + settings_path()); + tui.append_line(ICON_SYS + "Current settings:"); + tui.append_line(ICON_SYS + " model_path : " + cfg.model_path); + tui.append_line(ICON_SYS + " embed_path : " + cfg.embed_path); + tui.append_line(ICON_SYS + " sandbox : " + cfg.sandbox); + tui.append_line(ICON_SYS + " n_ctx : " + std::to_string(cfg.n_ctx)); + tui.append_line(ICON_SYS + " n_gpu_layers : " + std::to_string(cfg.n_gpu_layers)); + tui.append_line(ICON_SYS + " temperature : " + std::to_string(cfg.temperature)); + tui.append_line(ICON_SYS + " top_p : " + std::to_string(cfg.top_p)); + tui.append_line(ICON_SYS + " top_k : " + std::to_string(cfg.top_k)); + tui.append_line(ICON_SYS + " penalty_repeat: " + std::to_string(cfg.penalty_repeat)); + tui.append_line(ICON_SYS + " rag_top_k : " + std::to_string(cfg.rag_top_k)); + tui.append_line(ICON_SYS + " saved to : " + settings_path()); tui.redraw_all(); return; } @@ -2483,7 +2499,7 @@ static void handle_slash(const std::string &input, val.erase(0, val.find_first_not_of(" \t")); if (key.empty() || val.empty()) { - tui.append_line("[⚠] Usage: /set "); + tui.append_line(ICON_ERR + "Usage: /set "); tui.redraw_all(); return; } @@ -2496,11 +2512,10 @@ static void handle_slash(const std::string &input, else if (key == "top_k") { cfg.top_k = std::stoi(val); needs_reparam = true; } else if (key == "penalty_repeat") { cfg.penalty_repeat = std::stof(val); needs_reparam = true; } else if (key == "penalty_last_n") { cfg.penalty_last_n = std::stoi(val); needs_reparam = true; } - else if (key == "n_max_tokens") { cfg.n_max_tokens = std::stoi(val); needs_reparam = true; } else if (key == "rag_top_k") { cfg.rag_top_k = std::stoi(val); } else if (key == "n_gpu_layers") { cfg.n_gpu_layers = std::stoi(val); - tui.append_line("[sys] n_gpu_layers will take effect on next /model load."); + tui.append_line(ICON_SYS + "n_gpu_layers will take effect on next /model load."); } else if (key == "run_allowed") { // Accept a comma-separated list of basenames, or "none" to clear. cfg.run_allowed.clear(); @@ -2514,18 +2529,18 @@ static void handle_slash(const std::string &input, } } if (cfg.run_allowed.empty()) { - tui.append_line("[sys] run_allowed cleared β€” all sandbox programs permitted."); + tui.append_line(ICON_SYS + "run_allowed cleared β€” all sandbox programs permitted."); } else { std::string list; for (const auto &e : cfg.run_allowed) list += e + " "; - tui.append_line("[sys] run_allowed: " + list); + tui.append_line(ICON_SYS + "run_allowed: " + list); } } else { - tui.append_line("[⚠] Unknown key '" + key + "'. Try /help for list."); + tui.append_line(ICON_ERR + "Unknown key '" + key + "'. Try /help for list."); ok = false; } } catch (const std::exception &ex) { - tui.append_line(std::string("[⚠] /set: ") + ex.what()); + tui.append_line(ICON_ERR + "/set: " + ex.what()); ok = false; } @@ -2534,13 +2549,13 @@ static void handle_slash(const std::string &input, agent.apply_generation_params(cfg); } save_settings(cfg); - tui.append_line("[sys] " + key + " = " + val); + tui.append_line(ICON_SYS + "" + key + " = " + val); } tui.redraw_all(); return; } - tui.append_line("[⚠] Unknown command: " + verb + " (try /help)"); + tui.append_line(ICON_ERR + "Unknown command: " + verb + " (try /help)"); tui.redraw_all(); } @@ -2557,8 +2572,8 @@ static void welcome(TuiState &tui, const std::string &sandbox) { tui.append_line("[logo_5] β•šβ•β• β•šβ•β•β•β•β•šβ•β• β•šβ•β• β•šβ•β• β•šβ•β• β•šβ•β•β•β•β•β• "); tui.append_line("[logo_6] ─────────── agentic LLM shell v1.0 ──────────────"); tui.append_line(""); - tui.append_line("[sys] Sandbox : " + sandbox); - tui.append_line("[sys] /help for commands Β· exit to quit"); + tui.append_line(ICON_SYS + " Sandbox : " + sandbox); + tui.append_line(ICON_SYS + " /help for commands Β· exit to quit"); tui.append_line(""); tui.redraw_all(); } @@ -2674,9 +2689,9 @@ int main(int argc, char **argv) { agent.setup_embed(cfg.embed_path, tui); } } else { - tui.append_line("[sys] No model specified. Use /model to open the file picker,"); - tui.append_line("[sys] or /model to load directly."); - tui.append_line("[sys] Example: /model ~/models/qwen2.5-7b-q4_k_m.gguf"); + tui.append_line(ICON_SYS + "No model specified. Use /model to open the file picker,"); + tui.append_line(ICON_SYS + "or /model to load directly."); + tui.append_line(ICON_SYS + "Example: /model ~/models/qwen2.5-7b-q4_k_m.gguf"); tui.redraw_all(); }