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/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. diff --git a/configure.ac b/configure.ac index d4f1f4a..e3f5f79 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 @@ -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, @@ -37,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"], @@ -62,16 +79,30 @@ case "${host_os}" in WEBSOCKET_LDFLAGS="-lwsock32" GTK_SERVER_LDFLAGS="" GTK_SERVER_CPPFLAGS="-DGTK_SERVER_WIN32" + IOIO_CPPFLAGS="" + IOIO_LDFLAGS="" + NUKLEAR_CPPFLAGS="" ;; *) 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" + 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_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) @@ -82,6 +113,9 @@ AC_SUBST(WEBSOCKET_LDFLAGS) AC_SUBST(PLATFORM_LDFLAGS) 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/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/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/gtk-server/uthash b/gtk-server/uthash index ca98384..6d85739 160000 --- a/gtk-server/uthash +++ b/gtk-server/uthash @@ -1 +1 @@ -Subproject commit ca98384ce7f30beb216f9a0bc88a3b4340ead729 +Subproject commit 6d8573997c21f24c7e4ec9e48734b44f384170a1 diff --git a/include/javaproxy.h b/include/javaproxy.h new file mode 100644 index 0000000..c6de137 --- /dev/null +++ b/include/javaproxy.h @@ -0,0 +1,366 @@ +// 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; + +#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), + _arraySize(0) { + } + + 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)V"); + 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; + } + + // populate the java byte array with the contents of the basic array + 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(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 < 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 < arraySize; i++, j++) { + elements[j] = get_param_int(argc, params, i, 0); + } + result = argc - offset; + } + // make the changes available to the java side + g_env->ReleaseByteArrayElements(_array, elements, 0); + return result; + } + + protected: + jclass _clazz; + jobject _instance; + jbyteArray _array; + int _arraySize; +}; + +#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, bool debug) { + 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; + 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 && + g_jvm->AttachCurrentThread((void **)&g_env, nullptr) == JNI_OK); + if (!result) { + fprintf(stderr, "Failed to create JVM\n"); + } + return result; +} + +#endif diff --git a/include/log.h b/include/log.h new file mode 100644 index 0000000..7c33928 --- /dev/null +++ b/include/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/include/module.h b/include/module.h index d27ea52..5a81801 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_has_window_ui(void); + /** * @ingroup modstd * @@ -111,12 +120,22 @@ 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 */ -void sblib_free(int cls_id, int id); +int sblib_refresh_id(int cls_id, int id); /** * @ingroup modlib diff --git a/include/param.cpp b/include/param.cpp index 8f979c2..5a5fea1 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) { @@ -585,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/ioio/Makefile.am b/ioio/Makefile.am index 5650b4f..c2065a8 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 @@ -19,13 +19,23 @@ 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 = -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@ -L/usr/lib/jvm/java-1.8.0-openjdk-amd64/jre/lib/amd64/server -ljvm +libioio_la_LDFLAGS = -module -rpath '$(libdir)' @PLATFORM_LDFLAGS@ @JVM_LDFLAGS@ $(generated): api.json mkapi.bas mkdoc.bas $(sbasic) mkapi.bas > $@ $(sbasic) mkdoc.bas > README.md @touch main.cpp + +android: + @./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/ + +desktop: all + @(cd ioio && mvn clean package && cp target/ioio-1.0-jar-with-dependencies.jar ..) + 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/README.md b/ioio/README.md index 5d1bcd9..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 @@ -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 | |---------|---------------| @@ -100,19 +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(pin)` +`io = ioio.openTwiMaster(TWINumber, mode)` | Name | Description | |---------|---------------| +|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 bc77970..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" @@ -229,6 +229,8 @@ { "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": [ { "name": "setDutyCycle", @@ -246,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/build.gradle b/ioio/build.gradle new file mode 100644 index 0000000..2b1976e --- /dev/null +++ b/ioio/build.gradle @@ -0,0 +1,28 @@ +buildscript { + // configure the repositories and dependencies for Gradle itself + repositories { + google() + mavenCentral() + gradlePluginPortal() + } + + dependencies { + classpath 'com.android.tools.build:gradle:8.3.0' + classpath "com.github.johnrengelman:shadow:8.1.1" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'idea' +apply plugin: 'com.github.johnrengelman.shadow' + +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..537612e --- /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) +LOCAL_PATH := $(JNI_PATH) + +include $(CLEAR_VARS) +LOCAL_MODULE := ioio +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 \ + ../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) + diff --git a/ioio/ioio/build.gradle b/ioio/ioio/build.gradle new file mode 100644 index 0000000..721497e --- /dev/null +++ b/ioio/ioio/build.gradle @@ -0,0 +1,70 @@ +plugins { + id 'com.android.library' + id 'com.github.johnrengelman.shadow' +} + +android { + sourceSets { + main { + java { + exclude 'ioio/smallbasic/pc/**' + } + } + } + + namespace 'ioio.smallbasic' + compileSdk 34 + + defaultConfig { + minSdk 19 + } + + buildTypes { + release { + ndk { + debugSymbolLevel = 'FULL' + } + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + externalNativeBuild { + ndkBuild { + path './Android.mk' + } + } +} + +dependencies { + api 'com.github.ytai.ioio:IOIOLibCore:5.07' + shadow 'com.github.ytai.ioio:IOIOLibCore:5.07' +} + +// +// 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)) + } +} + +mergeJars { + from 'build/libs/ioio.jar' + from 'build/intermediates/aar_main_jar/release/syncReleaseLibJars/classes.jar' +} diff --git a/ioio/pom.xml b/ioio/ioio/pom.xml similarity index 74% rename from ioio/pom.xml rename to ioio/ioio/pom.xml index e81fb20..8f2333d 100644 --- a/ioio/pom.xml +++ b/ioio/ioio/pom.xml @@ -17,13 +17,22 @@ 5.07 - com.github.purejavacomm - purejavacomm - 1.0.2.RELEASE + com.fazecast + jSerialComm + 2.10.4 + + org.apache.maven.plugins + maven-compiler-plugin + + + ioio/smallbasic/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/AndroidManifest.xml b/ioio/ioio/src/main/AndroidManifest.xml new file mode 100644 index 0000000..793b961 --- /dev/null +++ b/ioio/ioio/src/main/AndroidManifest.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + 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 74% 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..29cc48b 100644 --- a/ioio/src/main/java/ioio/lib/spi/LogImpl.java +++ b/ioio/ioio/src/main/java/ioio/lib/spi/LogImpl.java @@ -32,9 +32,25 @@ public class LogImpl implements ILogger { private static final char[] LEVELS = {'0', '1', 'V', 'D', 'I', 'W', 'E', 'F'}; + private static final ILogger logger = getAndroidLogger(); @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.smallbasic.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/ioio/smallbasic/AnalogInputImpl.java similarity index 94% rename from ioio/src/main/java/net/sourceforge/smallbasic/ioio/AnalogInputImpl.java rename to ioio/ioio/src/main/java/ioio/smallbasic/AnalogInputImpl.java index a06817b..5420e10 100644 --- a/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; @@ -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/src/main/java/net/sourceforge/smallbasic/ioio/CapSenseImpl.java b/ioio/ioio/src/main/java/ioio/smallbasic/CapSenseImpl.java similarity index 93% rename from ioio/src/main/java/net/sourceforge/smallbasic/ioio/CapSenseImpl.java rename to ioio/ioio/src/main/java/ioio/smallbasic/CapSenseImpl.java index bf8a773..964df0b 100644 --- a/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; @@ -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/ioio/smallbasic/ConnectionController.java b/ioio/ioio/src/main/java/ioio/smallbasic/ConnectionController.java new file mode 100644 index 0000000..68690de --- /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 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[] { ACCESSORY_BOOTSTRAP }); + } else { + IOIOConnectionRegistry.addBootstraps(new String[] { SERIAL_PORT_BOOTSTRAP }); + } + } + + public ConnectionController(IOIOLooperProvider provider) { + super(provider); + } + + public void start() { + if (isAndroid) { + permitAccessory(); + } + manager.start(); + } + + public void stop() { + manager.stop(); + } + + private static boolean getIsAndroidBuild() { + boolean result; + try { + Class.forName(ACCESSORY_BOOTSTRAP); + result = true; + } catch (ClassNotFoundException e) { + result = false; + } + return result; + } + + private static void permitAccessory() { + try { + Class.forName(PERMISSION_CHECK).newInstance(); + } + catch (Exception e) { + throw new IOIOException(e); + } + } +} diff --git a/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/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/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/src/main/java/net/sourceforge/smallbasic/ioio/DigitalInputImpl.java b/ioio/ioio/src/main/java/ioio/smallbasic/DigitalInputImpl.java similarity index 91% rename from ioio/src/main/java/net/sourceforge/smallbasic/ioio/DigitalInputImpl.java rename to ioio/ioio/src/main/java/ioio/smallbasic/DigitalInputImpl.java index 32a4717..cd60df3 100644 --- a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/DigitalInputImpl.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/DigitalInputImpl.java @@ -1,12 +1,12 @@ -package net.sourceforge.smallbasic.ioio; +package ioio.smallbasic; + +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(); @@ -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/src/main/java/net/sourceforge/smallbasic/ioio/DigitalOutputImpl.java b/ioio/ioio/src/main/java/ioio/smallbasic/DigitalOutputImpl.java similarity index 90% rename from ioio/src/main/java/net/sourceforge/smallbasic/ioio/DigitalOutputImpl.java rename to ioio/ioio/src/main/java/ioio/smallbasic/DigitalOutputImpl.java index d6e81b3..98acbfb 100644 --- a/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; @@ -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/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/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/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..33a253d --- /dev/null +++ b/ioio/ioio/src/main/java/ioio/smallbasic/IOIOException.java @@ -0,0 +1,20 @@ +package ioio.smallbasic; + +public class IOIOException extends RuntimeException { + public IOIOException() { + super(); + } + + public IOIOException(Exception exception) { + this(exception.getMessage()); + } + + public IOIOException(String message) { + super(message); + IOUtil.setError(message); + } + + public String getMessage() { + return IOUtil.getError(); + } +} diff --git a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOImpl.java b/ioio/ioio/src/main/java/ioio/smallbasic/IOIOImpl.java similarity index 82% rename from ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOImpl.java rename to ioio/ioio/src/main/java/ioio/smallbasic/IOIOImpl.java index 4bf5de0..6f4bf9e 100644 --- a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOIOImpl.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/IOIOImpl.java @@ -1,8 +1,7 @@ -package net.sourceforge.smallbasic.ioio; +package ioio.smallbasic; import java.io.IOException; -import ioio.TimerUtil; import ioio.lib.api.IOIO; import ioio.lib.spi.Log; @@ -20,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); } @@ -55,10 +59,13 @@ public void sync() { lock.invoke(IOIO::sync); } - public void waitForConnect(int latency) { - TimerUtil.setLatency(latency); + public void waitForConnect() { + IOUtil.setError(null); IOService.getInstance().start(); + handleError(); lock.invoke(IOIO::waitForConnect); + handleError(); + IOUtil.setHardReset(false); } public void waitForDisconnect() { diff --git a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOLock.java b/ioio/ioio/src/main/java/ioio/smallbasic/IOLock.java similarity index 70% rename from ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOLock.java rename to ioio/ioio/src/main/java/ioio/smallbasic/IOLock.java index 1366f91..6ded9bf 100644 --- a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOLock.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/IOLock.java @@ -1,6 +1,9 @@ -package net.sourceforge.smallbasic.ioio; +package ioio.smallbasic; 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; import ioio.lib.api.exception.ConnectionLostException; @@ -8,6 +11,7 @@ public class IOLock { private final Object mutex = new Object(); + private static final int TIMEOUT_SECS = 10; private Consumer consumer; public void invoke(Consumer consumer) { @@ -36,7 +40,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)); @@ -53,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; } @@ -72,9 +89,11 @@ private CountDownLatch beginLatch() { */ private void endLatch(CountDownLatch latch) { try { - latch.await(); + if (!latch.await(TIMEOUT_SECS, TimeUnit.SECONDS)) { + throw new IOIOException("Timeout waiting for device"); + } } catch (InterruptedException e) { - throw new RuntimeException(e); + throw new IOIOException(e); } } } diff --git a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOService.java b/ioio/ioio/src/main/java/ioio/smallbasic/IOService.java similarity index 77% rename from ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOService.java rename to ioio/ioio/src/main/java/ioio/smallbasic/IOService.java index 3dc6c83..da025fd 100644 --- a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOService.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/IOService.java @@ -1,10 +1,10 @@ -package net.sourceforge.smallbasic.ioio; +package ioio.smallbasic; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; -import ioio.TimerUtil; import ioio.lib.api.IOIO; import ioio.lib.api.exception.ConnectionLostException; import ioio.lib.spi.Log; @@ -14,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; @@ -28,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(); @@ -35,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); @@ -56,13 +65,20 @@ public void start() { connectionController.start(); } + public void stop() { + if (looper.ioio != null) { + looper.ioio.disconnect(); + } + connectionController.stop(); + } + 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; } @@ -70,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() { @@ -90,12 +105,12 @@ 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(); } catch (Throwable e) { - next.setError(e.getLocalizedMessage()); + IOUtil.setError(e.getLocalizedMessage()); break; } } @@ -104,12 +119,11 @@ 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); } catch (Throwable e) { - next.setError(e.getLocalizedMessage()); + IOUtil.setError(e.getLocalizedMessage()); break; } } diff --git a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOTask.java b/ioio/ioio/src/main/java/ioio/smallbasic/IOTask.java similarity index 79% rename from ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOTask.java rename to ioio/ioio/src/main/java/ioio/smallbasic/IOTask.java index 28c3552..f04fb5f 100644 --- a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/IOTask.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/IOTask.java @@ -1,8 +1,7 @@ -package net.sourceforge.smallbasic.ioio; +package ioio.smallbasic; 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 RuntimeException(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..ce3f4c3 --- /dev/null +++ b/ioio/ioio/src/main/java/ioio/smallbasic/IOUtil.java @@ -0,0 +1,33 @@ +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 + } + + 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() + "]"); + } else { + ERROR.set(error); + } + } + + public static void setHardReset(boolean hardReset) { + HARD_RESET.set(hardReset); + } +} diff --git a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/PulseInputImpl.java b/ioio/ioio/src/main/java/ioio/smallbasic/PulseInputImpl.java similarity index 93% rename from ioio/src/main/java/net/sourceforge/smallbasic/ioio/PulseInputImpl.java rename to ioio/ioio/src/main/java/ioio/smallbasic/PulseInputImpl.java index 44a75e3..c9d2cf7 100644 --- a/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; @@ -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/src/main/java/net/sourceforge/smallbasic/ioio/PwmOutputImpl.java b/ioio/ioio/src/main/java/ioio/smallbasic/PwmOutputImpl.java similarity index 93% rename from ioio/src/main/java/net/sourceforge/smallbasic/ioio/PwmOutputImpl.java rename to ioio/ioio/src/main/java/ioio/smallbasic/PwmOutputImpl.java index 9303f78..62e07ad 100644 --- a/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; @@ -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/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/ioio/smallbasic/SpiMasterImpl.java b/ioio/ioio/src/main/java/ioio/smallbasic/SpiMasterImpl.java new file mode 100644 index 0000000..4ab01b3 --- /dev/null +++ b/ioio/ioio/src/main/java/ioio/smallbasic/SpiMasterImpl.java @@ -0,0 +1,107 @@ +package ioio.smallbasic; + +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 = 62; + private final byte[] BATCH = new byte[SPI_WRITE_MAX]; + private final IOLock lock = new IOLock<>(); + private SpiMaster spiMaster = null; + private int miso; + private int mosi; + private int clk; + private int slaveSelect; + + public SpiMasterImpl() { + super(); + Log.i(TAG, "created"); + } + + @Override + public void close() { + super.close(); + if (spiMaster != null) { + spiMaster.close(); + spiMaster = null; + } + } + + public void open(int miso, int mosi, int clk, int slaveSelect) throws IOException { + super.open(miso); + this.miso = miso; + this.mosi = mosi; + this.clk = clk; + this.slaveSelect = slaveSelect; + validatePins(); + } + + 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) -> { + if (writeLen > SPI_WRITE_MAX) { + int srcPos = 0; + while (srcPos < writeLen) { + int batchLen = Math.min(writeLen - srcPos, 2); + System.arraycopy(write, srcPos, BATCH, 0, batchLen); + spiMaster.writeReadAsync(0, BATCH, batchLen, batchLen, null, 0); + srcPos += batchLen; + } + } else { + spiMaster.writeRead(write, writeLen, writeLen, null, 0); + } + }); + } + + @Override + void loop() throws ConnectionLostException, InterruptedException { + lock.process(spiMaster); + } + + @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_4M); + } + + private void pinError(String name) { + IOUtil.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) { + IOUtil.setError("One or more pins have duplicate values"); + } + } +} diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/TwiMasterImpl.java b/ioio/ioio/src/main/java/ioio/smallbasic/TwiMasterImpl.java new file mode 100644 index 0000000..fc0c58f --- /dev/null +++ b/ioio/ioio/src/main/java/ioio/smallbasic/TwiMasterImpl.java @@ -0,0 +1,68 @@ +package ioio.smallbasic; + +import java.io.IOException; + +import ioio.lib.api.IOIO; +import ioio.lib.api.TwiMaster; +import ioio.lib.api.exception.ConnectionLostException; +import ioio.lib.spi.Log; + +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 int twiNum; + private boolean smbus; + + public TwiMasterImpl() { + super(); + Log.i(TAG, "created"); + } + + @Override + public void close() { + super.close(); + if (twiMaster != null) { + twiMaster.close(); + twiMaster = null; + } + } + + public void open(int twiNum, int smbus) throws IOException { + super.open(twiNum); + this.twiNum = twiNum; + this.smbus = (smbus == 1); + } + + public long readWrite(int address, int readLen, final byte[] write, int writeLen) { + handleError(); + 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)Byte.toUnsignedInt(read[index])) << (index * 8); + } + return result; + }); + } + + public void write(int address, final byte[] write, int writeLen) { + handleError(); + lock.invoke((i) -> { + twiMaster.writeRead(address, false, write, writeLen, null, 0); + }); + } + + @Override + void loop() throws ConnectionLostException, InterruptedException { + lock.process(twiMaster); + } + + @Override + void setup(IOIO ioio) throws ConnectionLostException { + Log.i(TAG, "setup entered: " + twiNum); + twiMaster = ioio.openTwiMaster(twiNum, rate, smbus); + } +} diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryConnectionBootstrap.java b/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryConnectionBootstrap.java new file mode 100644 index 0000000..1364c44 --- /dev/null +++ b/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryConnectionBootstrap.java @@ -0,0 +1,62 @@ +/* + * 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 java.util.Collection; + +import ioio.lib.api.IOIOConnection; +import ioio.lib.spi.IOIOConnectionBootstrap; +import ioio.lib.spi.IOIOConnectionFactory; + +public class AccessoryConnectionBootstrap implements IOIOConnectionBootstrap, IOIOConnectionFactory { + public AccessoryConnectionBootstrap() { + super(); + } + + @Override + public IOIOConnection createConnection() { + return new UsbConnection(); + } + + @Override + public Object getExtra() { + return null; + } + + @Override + public void getFactories(Collection result) { + result.add(this); + } + + @Override + public String getType() { + 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 new file mode 100644 index 0000000..3189bda --- /dev/null +++ b/ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionCheck.java @@ -0,0 +1,65 @@ +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 android.widget.Toast; + +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 = "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"); + UsbAccessory accessory = UsbUtil.getUsbAccessory(); + if (accessory == null || !"IOIO".equals(accessory.getModel())) { + throw new IOIOException("IOIO board not found."); + } + + UsbManager usbManager = UsbUtil.getUsbManager(); + if (!usbManager.hasPermission(accessory)) { + new Handler(Looper.getMainLooper()).post(() -> { + 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); + int flags = PendingIntent.FLAG_IMMUTABLE; + Intent intent = new Intent(ACTION_USB_PERMISSION); + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, flags); + usbManager.requestPermission(accessory, pendingIntent); + }); + // for some reason using a latch here causes an ANR + Log.d(TAG, "requesting permission"); + throw new IOIOException(PERMISSION_ERROR); + } + } + + @Override + public synchronized void onReceive(final Context context, Intent intent) { + Log.d(TAG, "onReceive entered"); + if (ACTION_USB_PERMISSION.equals(intent.getAction())) { + 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(); + context.unregisterReceiver(receiver); + }); + } + } +} diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/android/AndroidLogger.java b/ioio/ioio/src/main/java/ioio/smallbasic/android/AndroidLogger.java new file mode 100644 index 0000000..64f93bf --- /dev/null +++ b/ioio/ioio/src/main/java/ioio/smallbasic/android/AndroidLogger.java @@ -0,0 +1,19 @@ +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 { + public AndroidLogger() { + super(); + } + + @TargetApi(Build.VERSION_CODES.O) + @Override + public void write(int level, String tag, String 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/ModuleLoader.java b/ioio/ioio/src/main/java/ioio/smallbasic/android/ModuleLoader.java new file mode 100644 index 0000000..99b2161 --- /dev/null +++ b/ioio/ioio/src/main/java/ioio/smallbasic/android/ModuleLoader.java @@ -0,0 +1,28 @@ +package ioio.smallbasic.android; + +import android.content.Context; + +import java.lang.ref.WeakReference; + +import ioio.lib.spi.Log; + +/** + * ModuleLoader - Invoked from the "app" to commence loading + */ +public class ModuleLoader { + private static final String TAG = "ModuleLoader"; + public static native void init(Long app); + + private static WeakReference context; + + public ModuleLoader(Long activity, Context context) { + super(); + Log.d(TAG, "ModuleLoader: " + activity); + ModuleLoader.context = new WeakReference<>(context); + init(activity); + } + + public static Context getContext() { + return context.get(); + } +} diff --git a/ioio/ioio/src/main/java/ioio/smallbasic/android/UsbConnection.java b/ioio/ioio/src/main/java/ioio/smallbasic/android/UsbConnection.java new file mode 100644 index 0000000..666231c --- /dev/null +++ b/ioio/ioio/src/main/java/ioio/smallbasic/android/UsbConnection.java @@ -0,0 +1,199 @@ +/* + * 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.impl.FixedReadBufferedInputStream; +import ioio.lib.spi.Log; +import ioio.smallbasic.IOIOException; +import ioio.smallbasic.IOUtil; + +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; + private OutputStream outputStream; + private ParcelFileDescriptor fileDescriptor; + + private enum ConnectionState { + INIT, CONNECTED, DISCONNECTED + } + + UsbConnection() { + Log.d(TAG, "creating UsbConnection"); + this.state = ConnectionState.INIT; + this.inputStream = null; + this.outputStream = null; + this.fileDescriptor = null; + } + + @Override + public boolean canClose() { + return false; + } + + @Override + public synchronized void disconnect() { + Log.d(TAG, "disconnect entered: " + state); + if (state != ConnectionState.DISCONNECTED) { + IOUtil.setError("USB disconnected"); + close(); + } + Log.d(TAG, "leaving disconnect"); + } + + @Override + public InputStream getInputStream() { + return inputStream; + } + + @Override + public OutputStream getOutputStream() { + return outputStream; + } + + @Override + public synchronized void waitForConnect() { + if (state != ConnectionState.INIT) { + throw new IllegalStateException("waitForConnect() may only be called once"); + } + if (open()) { + state = ConnectionState.CONNECTED; + } else { + throw new IOIOException("USB connection lost"); + } + } + + @Override + protected void finalize() { + disconnect(); + } + + private void close() { + Log.d(TAG, "close streams"); + 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 (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 < MAX_RETRIES) { + int response = inputStream.read(); + Log.d(TAG, "Response:" + response + " available:" + inputStream.available() + " attempt:" + attempt); + if (response != SOFT_RESET) { + // unexpected + if (inputStream.available() < 1) { + 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.d(TAG, "open() entered"); + + try { + openStreams(); + resetBoard(); + handleResetResponse(0); + result = true; + } catch (java.io.IOException e) { + IOUtil.setError("Failed to open streams: " + e); + } finally { + if (!result) { + 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()) { + Log.d(TAG, "hard reset"); + outputStream.write(HARD_RESET); + outputStream.write('I'); + outputStream.write('O'); + outputStream.write('I'); + outputStream.write('O'); + } else { + Log.d(TAG, "soft reset"); + outputStream.write(SYNC); + } + outputStream.flush(); + } +} 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..9ca96c2 --- /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) ModuleLoader.getContext().getSystemService(Context.USB_SERVICE); + UsbAccessory[] accessories = usbManager.getAccessoryList(); + return (accessories == null ? null : accessories[0]); + } + + static UsbManager getUsbManager() { + Context activity = ModuleLoader.getContext(); + return (UsbManager) activity.getSystemService(Context.USB_SERVICE); + } +} 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 new file mode 100644 index 0000000..fcc519f --- /dev/null +++ b/ioio/ioio/src/main/java/ioio/smallbasic/pc/SerialPortIOIOConnection.java @@ -0,0 +1,72 @@ +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; + +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 boolean canClose() { + return serialPort.isOpen(); + } + + @Override + synchronized public void disconnect() { + abort = true; + if (serialPort != null && inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + Log.i(TAG, e.toString()); + } + serialPort.closePort(); + } + } + + @Override + public InputStream getInputStream() throws ConnectionLostException { + if (inputStream == null) { + throw new ConnectionLostException(); + } + return inputStream; + } + + @Override + public OutputStream getOutputStream() throws ConnectionLostException { + if (outputStream == null) { + throw new ConnectionLostException(); + } + return outputStream; + } + + @Override + public void waitForConnect() throws ConnectionLostException { + if (!abort && serialPort.openPort()) { + inputStream = new LoggingInputStream(serialPort.getInputStream()); + outputStream = new LoggingOutputStream(serialPort.getOutputStream()); + serialPort.setDTR(); + } else { + throw new ConnectionLostException(); + } + } +} diff --git a/ioio/src/main/java/ioio/lib/pc/SerialPortIOIOConnectionBootstrap.java b/ioio/ioio/src/main/java/ioio/smallbasic/pc/SerialPortIOIOConnectionBootstrap.java similarity index 69% rename from ioio/src/main/java/ioio/lib/pc/SerialPortIOIOConnectionBootstrap.java rename to ioio/ioio/src/main/java/ioio/smallbasic/pc/SerialPortIOIOConnectionBootstrap.java index 7d50fb1..eaec883 100644 --- a/ioio/src/main/java/ioio/lib/pc/SerialPortIOIOConnectionBootstrap.java +++ b/ioio/ioio/src/main/java/ioio/smallbasic/pc/SerialPortIOIOConnectionBootstrap.java @@ -27,86 +27,41 @@ * or implied. */ -package ioio.lib.pc; +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 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; + } } 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 @@ + + + + diff --git a/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/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/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/src/test/java/net/sourceforge/smallbasic/ioio/DigitalOutputTest.java b/ioio/ioio/src/test/java/ioio/smallbasic/DigitalOutputTest.java similarity index 93% rename from ioio/src/test/java/net/sourceforge/smallbasic/ioio/DigitalOutputTest.java rename to ioio/ioio/src/test/java/ioio/smallbasic/DigitalOutputTest.java index d4bc098..a72b613 100644 --- a/ioio/src/test/java/net/sourceforge/smallbasic/ioio/DigitalOutputTest.java +++ b/ioio/ioio/src/test/java/ioio/smallbasic/DigitalOutputTest.java @@ -1,4 +1,4 @@ -package net.sourceforge.smallbasic.ioio; +package ioio.smallbasic; import java.io.IOException; diff --git a/ioio/main.cpp b/ioio/main.cpp index e8b6efd..8187701 100644 --- a/ioio/main.cpp +++ b/ioio/main.cpp @@ -6,245 +6,147 @@ // Copyright(C) 2024 Chris Warren-Smith #include "config.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 *env; -JavaVM *jvm; -IOTask *ioioTask; -int nextId = 1; - -#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" +#include "include/log.h" +#include "include/var.h" +#include "include/module.h" +#include "include/param.h" +#include "include/javaproxy.h" + +#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 SPI_WRITE_MAX 62 +#define TWI_WRITE_MAX 255 -struct IOTask { - IOTask(): _clazz(nullptr), _instance(nullptr) {} - - virtual ~IOTask() { - _clazz = nullptr; - _instance = nullptr; - } - - bool create(const char *path) { - bool result; - if (_instance != nullptr) { - // error when already constructed - result = false; - } else { - _clazz = env->FindClass(path); - if (_clazz == nullptr) { - env->ExceptionDescribe(); - } else { - jmethodID constructor = env->GetMethodID(_clazz, "", "()V"); - if (constructor == nullptr) { - env->ExceptionDescribe(); - } else { - _instance = env->NewObject(_clazz, constructor); - } - } - result = _instance != nullptr; - } - return result; +struct IOTask : JavaProxy { + IOTask() : JavaProxy() { } - bool checkException(var_s *retval) { - auto exc = 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); - error(retval, message); - env->ReleaseStringUTFChars(jstr, message); - } else { - env->ExceptionDescribe(); - env->ExceptionClear(); - } - } - return exc; + int open(int pin, var_s *retval) { + return invokeVoidInt("open", pin, retval); } - // boolean foo(void) - int invokeBoolVoid(const char *name, var_s *retval) { - int result = 0; - if (_instance != nullptr) { - jmethodID method = env->GetMethodID(_clazz, name, "()Z"); - int value = 0; - if (method != nullptr) { - value = env->CallBooleanMethod(_instance, method); - } - if (!checkException(retval)) { - v_setint(retval, value); - result = 1; - } - } - return result; + int open2(int pin1, int pin2, var_s *retval) { + return invokeVoidInt2("open", pin1, pin2, retval); } - // float foo(void) - int invokeFloatVoid(const char *name, var_s *retval) { - int result = 0; - if (_instance != nullptr) { - jmethodID method = env->GetMethodID(_clazz, name, "()F"); - var_num_t value = 0; - if (method != nullptr) { - value = env->CallFloatMethod(_instance, method); - } - if (!checkException(retval)) { - v_setreal(retval, value); - result = 1; - } - } - return result; + int open4(int pin1, int pin2, int pin3, int pin4, var_s *retval) { + return invokeVoidInt4("open", pin1, pin2, pin3, pin4, retval); } - // int foo(void) - int invokeIntVoid(const char *name, var_s *retval) { + // int readWrite(bytes, byte[] write) { + int invokeSpiReadWrite(int argc, slib_par_t *arg, var_s *retval) { int result = 0; - if (_instance != nullptr) { - jmethodID method = env->GetMethodID(_clazz, name, "()I"); - int value = 0; + 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) { + attachCurrentThread(); + jmethodID method = g_env->GetMethodID(_clazz, "readWrite", "(I[BI)J"); + var_int_t value = 0; if (method != nullptr) { - value = env->CallIntMethod(_instance, method); + 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; } - // void foo(boolean) - int invokeVoidBool(const char *name, int value, var_s *retval) { - int result = 0; - if (_instance != nullptr) { - jmethodID method = env->GetMethodID(_clazz, name, "(Z)V"); - if (method != nullptr) { - env->CallVoidMethod(_instance, method, value); - } - if (!checkException(retval)) { - result = 1; - } - } - return result; - } - - // void foo(float) - 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"); - if (method != nullptr) { - env->CallVoidMethod(_instance, method, value); - } - if (!checkException(retval)) { - result = 1; - } - } - return result; - } - - // void foo(int) - int invokeVoidInt(const char *name, int value, var_s *retval) { + // int write(byte[] write, int length) { + int invokeSpiWrite(int argc, slib_par_t *arg, var_s *retval) { int result = 0; - if (_instance != nullptr) { - jmethodID method = env->GetMethodID(_clazz, name, "(I)V"); - if (method != nullptr) { - env->CallVoidMethod(_instance, method, value); - } - if (!checkException(retval)) { - result = 1; - } + 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); } - return result; - } - - 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"); + 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"); if (method != nullptr) { - env->CallVoidMethod(_instance, method, value1, value2); + g_env->CallVoidMethod(_instance, method, _array, writeLen); } if (!checkException(retval)) { result = 1; } + detachCurrentThread(); } return result; } - - int invokeVoidInt4(const char *name, int value1, int value2, int value3, int value4, var_s *retval) { + + // int readWrite(int address, byte[] write) { + int invokeTwiReadWrite(int argc, slib_par_t *arg, var_s *retval) { int result = 0; - if (_instance != nullptr) { - jmethodID method = env->GetMethodID(_clazz, name, "(IIII)V"); + 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) { + attachCurrentThread(); + jmethodID method = g_env->GetMethodID(_clazz, "readWrite", "(II[BI)J"); + var_int_t value = 0; if (method != nullptr) { - env->CallVoidMethod(_instance, method, value1, value2, value3, value4); + 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; } - // void foo(void) - int invokeVoidVoid(const char *name, var_s *retval) { + // int write(int address, byte[] write) { + int invokeTwiWrite(int argc, slib_par_t *arg, var_s *retval) { int result = 0; - if (_instance != nullptr) { - jmethodID method = env->GetMethodID(_clazz, name, "()V"); + 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) { + attachCurrentThread(); + jmethodID method = g_env->GetMethodID(_clazz, "write", "(I[BI)V"); if (method != nullptr) { - env->CallVoidMethod(_instance, method); + 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; } - - int open(int pin, var_s *retval) { - return invokeVoidInt("open", pin, retval); - } - - int open2(int pin1, int pin2, var_s *retval) { - return invokeVoidInt2("open", pin1, pin2, retval); - } - - 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; }; -robin_hood::unordered_map _ioTaskMap; +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; 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; } } @@ -254,38 +156,72 @@ 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_readwrite(var_s *self, int argc, slib_par_t *arg, var_s *retval) { int result = 0; - if (argc != 0) { - error(retval, "TwiMaster.writeRead", 0); + auto readBytes = get_param_int(argc, arg, 1, 0); + if (argc < 2) { + 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 { - // TODO - //result = ioioTask->invokeVoidVoid("waitForDisconnect", retval); + int id = get_io_class_id(self, retval); + if (id != -1) { + result = g_ioTaskMap.at(id).invokeTwiReadWrite(argc, arg, retval); + } + } + return result; +} + +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, TWI_WRITE_MAX); + } else { + int id = get_io_class_id(self, retval); + if (id != -1) { + 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, 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 { + int id = get_io_class_id(self, retval); + if (id != -1) { + result = g_ioTaskMap.at(id).invokeSpiReadWrite(argc, arg, retval); + } } return result; } 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); + if (argc < 1) { + error(retval, "SpiMaster.write", 1, SPI_WRITE_MAX); } 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 = g_ioTaskMap.at(id).invokeSpiWrite(argc, arg, retval); } } return result; } 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, "readWrite", cmd_twimaster_readwrite); } 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" @@ -296,7 +232,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}, }; @@ -308,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}, }; @@ -320,59 +256,78 @@ SBLIB_API int sblib_func_count() { return (sizeof(lib_func) / sizeof(lib_func[0])); } +// +// Program startup +// int sblib_init(const char *sourceFile) { - JavaVMInitArgs vm_args; - JavaVMOption options[2]; - 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 = "-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.options = options; - vm_args.ignoreUnrecognized = 0; - int result = (JNI_CreateJavaVM(&jvm, (void **)&env, &vm_args) == JNI_OK && - jvm->AttachCurrentThread((void **)&env, nullptr) == JNI_OK); - if (!result) { - fprintf(stderr, "Failed to create JVM\n"); - } - - ioioTask = new IOTask(); - if (!ioioTask || !ioioTask->create(CLASS_IOIO)) { - fprintf(stderr, "Failed to IOIOTask\n"); +#if defined(DESKTOP_MODULE) + int result = createJVM("-Djava.class.path=./ioio-1.0-jar-with-dependencies.jar", "-Dioio.SerialPorts=ttyACM0", false); +#else + int result = 1; +#endif + 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; } +#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: - if (id != -1 && _ioTaskMap.find(id) != _ioTaskMap.end()) { - _ioTaskMap.at(id).invokeVoidVoid("close", nullptr); - _ioTaskMap.erase(id); + if (g_ioTaskMap.find(id) != g_ioTaskMap.end()) { + g_ioTaskMap.at(id).invokeVoidVoid("close", nullptr); + g_ioTaskMap.erase(id); } break; } } } +// +// Program termination +// 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 (jvm) { - jvm->DetachCurrentThread(); +#if defined(DESKTOP_MODULE) + if (g_jvm) { + g_jvm->DetachCurrentThread(); } // calling this hangs //jvm->DestroyJavaVM(); - env = nullptr; - jvm = nullptr; + g_env = nullptr; + g_jvm = nullptr; +#endif } - diff --git a/ioio/mkapi.bas b/ioio/mkapi.bas index a6ba571..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 = "ioioTask->" + getter = "g_ioioTask->" indent = " " - else - getter = "_ioTaskMap.at(id)." + 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,18 +112,17 @@ 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 = ++nextId;" - print " IOTask &instance = _ioTaskMap[id];" - print " if (instance.create(CLASS_" + upper(obj.name) + ") &&" + 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 " error(retval, \"open" + obj.name + "() failed\");" + print " g_ioTaskMap.erase(id);" print " result = 0;" print " }" print " return result;" @@ -132,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 @@ -146,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/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/bme280.bas b/ioio/samples/bme280.bas new file mode 100644 index 0000000..e8889c7 --- /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 result.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/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" 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/duino-1088AS.bas b/ioio/samples/duino-1088AS.bas deleted file mode 100644 index 87da026..0000000 --- a/ioio/samples/duino-1088AS.bas +++ /dev/null @@ -1,23 +0,0 @@ -import ioio - -' mosi = master-in slave-out (miso is unused here) -const mosiPin = 2 -const misoPin = 6 -const csPin = 3 -const clkPin = 4 - -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(0x0B, 0x07) ' Scan limit: all digits -spi.write(0x0C, 0x01) ' Shutdown: normal operation - -while 1 - for i = 1 to 8 - spi.write(i, 0x35) - next - delay 1000 -wend -' diff --git a/ioio/samples/duino-1088as.bas b/ioio/samples/duino-1088as.bas new file mode 100644 index 0000000..60d5079 --- /dev/null +++ b/ioio/samples/duino-1088as.bas @@ -0,0 +1,61 @@ +import ioio + +' mosi = master-in slave-out (miso is unused here) +const mosiPin = 2 +const misoPin = 6 +const csPin = 3 +const clkPin = 4 + +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, 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 + print_glyph(glyph_a) + delay 1000 + print_glyph(glyph_b) + delay 1000 +wend 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/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 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" 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 diff --git a/ioio/samples/pwm.bas b/ioio/samples/pwm.bas new file mode 100644 index 0000000..c6ad84c --- /dev/null +++ b/ioio/samples/pwm.bas @@ -0,0 +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, 1000) + +print "wait for connect" +ioio.waitForConnect(10) +print "ready!!!" + +for DutyCycle = 0 to 1 step 0.01 + led.setDutyCycle(DutyCycle) + delay 10 +next + +print "Done" 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 diff --git a/ioio/samples/st7735s.bas b/ioio/samples/st7735s.bas new file mode 100644 index 0000000..3498891 --- /dev/null +++ b/ioio/samples/st7735s.bas @@ -0,0 +1,164 @@ +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_DISPOFF 0x28 +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 150 +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 + ' higher values didn't improve performance + local bufSize = 40 + + 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) / bufSize + 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() +randomize +t1 = timer +fill_LCD(rnd*255, rnd*255, rnd*255) +print format("Elap: ###", timer-t1) +delay 5000 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 diff --git a/ioio/samples/veml6030.bas b/ioio/samples/veml6030.bas new file mode 100644 index 0000000..2da76ab --- /dev/null +++ b/ioio/samples/veml6030.bas @@ -0,0 +1,42 @@ +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 + +import ioio + +rem +rem wiring: +rem SDA -> pin 1 +rem CLK -> pin 2 +rem + +rem i2c address +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 + +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) + +for i = 0 to 5 + print p3.readWrite(address, 2, [alsDataReg]) + delay 1000 +next 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' 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/TimerUtil.java b/ioio/src/main/java/ioio/TimerUtil.java deleted file mode 100644 index c6e9139..0000000 --- a/ioio/src/main/java/ioio/TimerUtil.java +++ /dev/null @@ -1,21 +0,0 @@ -package ioio; - -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/src/main/java/ioio/lib/pc/SerialPortIOIOConnection.java b/ioio/src/main/java/ioio/lib/pc/SerialPortIOIOConnection.java deleted file mode 100644 index 5c96e0f..0000000 --- a/ioio/src/main/java/ioio/lib/pc/SerialPortIOIOConnection.java +++ /dev/null @@ -1,205 +0,0 @@ -/* - * 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 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; - } - - @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(); - } - } - } - throw new ConnectionLostException(); - } - - @Override - synchronized public void disconnect() { - abort_ = true; - if (serialPort_ != null) { - try { - inputStream_.close(); - } catch (IOException e) { - } - serialPort_.close(); - } - } - - @Override - public InputStream getInputStream() throws ConnectionLostException { - return inputStream_; - } - - @Override - public OutputStream getOutputStream() throws 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/net/sourceforge/smallbasic/ioio/ConnectionController.java b/ioio/src/main/java/net/sourceforge/smallbasic/ioio/ConnectionController.java deleted file mode 100644 index c8471b9..0000000 --- a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/ConnectionController.java +++ /dev/null @@ -1,35 +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 { - IOIOConnectionRegistry.addBootstraps(new String[]{ - "ioio.lib.pc.SerialPortIOIOConnectionBootstrap" - }); - /* - IOIOConnectionRegistry.addBootstraps(new String[]{ - "ioio.lib.impl.SocketIOIOConnectionBootstrap", - "ioio.lib.android.accessory.AccessoryConnectionBootstrap", - "ioio.lib.android.bluetooth.BluetoothIOIOConnectionBootstrap", - "ioio.lib.android.device.DeviceConnectionBootstrap"}); - */ - } - - public ConnectionController(IOIOLooperProvider provider) { - super(provider); - } - - public void start() { - manager.start(); - } - - public void stop() { - manager.stop(); - } -} diff --git a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/SpiMasterImpl.java b/ioio/src/main/java/net/sourceforge/smallbasic/ioio/SpiMasterImpl.java deleted file mode 100644 index a831cdf..0000000 --- a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/SpiMasterImpl.java +++ /dev/null @@ -1,50 +0,0 @@ -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; - -public class SpiMasterImpl extends IOTask { - private static final String TAG = "SpiMasterImpl"; - private final IOLock lock = new IOLock<>(); - private SpiMaster spiMaster = null; - private int miso; - private int mosi; - private int clk; - private int slaveSelect; - - public SpiMasterImpl() { - super(); - Log.i(TAG, "created"); - } - - public void open(int miso, int mosi, int clk, int slaveSelect) throws IOException { - super.open(miso); - this.miso = miso; - this.mosi = mosi; - this.clk = clk; - this.slaveSelect = slaveSelect; - } - - public void write(int address, int data) { - handleError(); - lock.invoke((i) -> { - byte[] buffer = {(byte) address, (byte) data}; - spiMaster.writeRead(buffer, buffer.length, buffer.length, null, 0); - }); - } - - @Override - void loop() throws ConnectionLostException, InterruptedException { - lock.process(spiMaster); - } - - @Override - 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); - } -} diff --git a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java b/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java deleted file mode 100644 index 500eb5d..0000000 --- a/ioio/src/main/java/net/sourceforge/smallbasic/ioio/TwiMasterImpl.java +++ /dev/null @@ -1,35 +0,0 @@ -package net.sourceforge.smallbasic.ioio; - -import ioio.lib.api.IOIO; -import ioio.lib.api.TwiMaster; -import ioio.lib.api.exception.ConnectionLostException; -import ioio.lib.spi.Log; - -public class TwiMasterImpl extends IOTask { - private static final String TAG = "TwiMasterImpl"; - private TwiMaster twiMaster = null; - private TwiMaster.Rate rate = TwiMaster.Rate.RATE_100KHz; - private int twiNum; - private boolean smbus; - - public TwiMasterImpl() { - super(); - Log.i(TAG, "created"); - } - - public void open(int twiNum, int smbus) { - this.twiNum = twiNum; - this.smbus = (smbus == 1); - } - - @Override - void loop() throws ConnectionLostException, InterruptedException { - // TODO - } - - @Override - void setup(IOIO ioio) throws ConnectionLostException { - Log.i(TAG, "setup entered"); - twiMaster = ioio.openTwiMaster(twiNum, rate, smbus); - } -} diff --git a/llama/CMakeLists.txt b/llama/CMakeLists.txt new file mode 100644 index 0000000..a61dd32 --- /dev/null +++ b/llama/CMakeLists.txt @@ -0,0 +1,176 @@ +cmake_minimum_required(VERSION 3.15) +project(llm C CXX) + +# clang-check ../*.cpp +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_C_STANDARD 11) + +# ----------------------------- +# Path to llama.cpp +# ----------------------------- +set(LLAMA_DIR ${CMAKE_CURRENT_SOURCE_DIR}/llama.cpp) + +# ----------------------------- +# FORCE 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) + +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_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 +# ------------------------------- +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 +# ----------------------------- +add_subdirectory(${LLAMA_DIR}/ggml) +add_subdirectory(${LLAMA_DIR}) + +# ----------------------------- +# Build plugin as a shared library (.so) +# ----------------------------- +set(PLUGIN_SOURCES + main.cpp + llama-sb.cpp + ../include/param.cpp + ../include/hashmap.cpp + ../include/apiexec.cpp +) + +add_library(llm SHARED ${PLUGIN_SOURCES}) + +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 PRIVATE + llama + ggml + # force dynamic libm + -Wl,-Bdynamic,-lm +) + +# Include all static code into plugin +target_link_options(llm PRIVATE + -Wl,--whole-archive + $ + $ + -Wl,--no-whole-archive +) + +# Ensure position-independent code for .so +set_target_properties(llm PROPERTIES + POSITION_INDEPENDENT_CODE ON + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib +) + +# ------------------------------------------------------------------ +# 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 + 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/RAG.md b/llama/RAG.md new file mode 100644 index 0000000..6c69c06 --- /dev/null +++ b/llama/RAG.md @@ -0,0 +1,323 @@ +# 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 qwen3-embedding-0.6b-q4_k_m.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** — `qwen3-embedding-0.6b-q4_k_m.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 qwen3-embedding-0.6b-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 qwen3-embedding-0.6b-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/README.md b/llama/README.md new file mode 100644 index 0000000..29f99c8 --- /dev/null +++ b/llama/README.md @@ -0,0 +1,321 @@ +# 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. + +## 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) + +--- + +## System Requirements & CUDA Setup + +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 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 +``` + +### 3. Install CUDA Toolkit +Install only the toolkit (no driver replacement): +```bash +sudo apt install -y cuda-toolkit +``` +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. + +### 5. Verify Installation +```bash +nvcc --version +``` +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.* + +--- + +## Obtaining Models from Hugging Face + +The `LLAMA` function expects a path to a model file (e.g., `gguf` format). Models can be obtained from the Hugging Face Hub. + +### Method 1: Using `huggingface-cli` (Recommended) + +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 + ``` + +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.* + +### 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 +) +``` + +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) +``` + +### 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 + +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. + +--- + +## 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) +``` + +### 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) +``` + +### Interaction +The primary method of interaction is `add_message`, which sends a prompt to the model. + +```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.") +``` + +### Streaming Responses +The returned iterator allows real-time processing of the model's output: + +* `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) +llama.set_top_p(0.0) +llama.set_min_p(0.0) +``` + +### 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) +llama.set_top_p(0.0) +llama.set_min_p(0.05) +``` + +### 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) +``` + +### Technical & Conservative +*Best for: Documentation, logic, precise tasks.* +```basic +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 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) +' llama.set_top_p(0.0) +' llama.set_min_p(0.05) +``` + +--- + +## 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. | + +--- + +## 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 (Default) +*Recommended for general usage.* +```basic +llama.set_penalty_last_n(64) +llama.set_penalty_repeat(1.1) +``` + +### 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) +``` + +### 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/llama-sb-rag.cpp b/llama/llama-sb-rag.cpp new file mode 100644 index 0000000..0b11d04 --- /dev/null +++ b/llama/llama-sb-rag.cpp @@ -0,0 +1,445 @@ +// 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 "llama-sb-rag.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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"; + } +} + +/* ── 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; +} + +/* ── state machine ─────────────────────────────────────────── */ + +enum class State { + Idle, BlockComment, LineComment, Declaration, Struct, Defines +}; + +template + +static bool chunk_file(const fs::path &path, EmitChunk emit_chunk) { + std::ifstream f(path); + if (!f) { + return false; + } + + 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); + + 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(); +} + +// +// 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 +// +std::string Llama::rag_retrieve(const RagDB &db, + const std::string &query, + 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 {}; + } + + // 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); +} + +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 new file mode 100644 index 0000000..0296f26 --- /dev/null +++ b/llama/llama-sb-rag.h @@ -0,0 +1,78 @@ +// 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; +}; + +/* ── 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(); } +}; + +// +// 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 new file mode 100644 index 0000000..e1a07d5 --- /dev/null +++ b/llama/llama-sb.cpp @@ -0,0 +1,738 @@ +// 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 "ggml-cuda.h" + +#include "llama.h" +#include "llama-sb.h" + +constexpr int MAX_REPEAT = 50; + +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), + _tokens_generated(0), + _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), + _sampler(nullptr), + _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), + _top_k(0), + _max_tokens(0), + _log_level(GGML_LOG_LEVEL_CONT), + _n_gpu_layers(0), + _n_system_tokens(0), + _is_gemma4(false), + _sampler_dirty(false), + _can_shift(false), + _memory_flush(false), + _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 == 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); + } + }, this); + reset(); + 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)) + , _template(std::move(other._template)) + , _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) + , _top_k(other._top_k) + , _max_tokens(other._max_tokens) + , _log_level(other._log_level) + , _n_gpu_layers(other._n_gpu_layers) + , _n_system_tokens(other._n_system_tokens) + , _is_gemma4(other._is_gemma4) + , _sampler_dirty(other._sampler_dirty) + , _can_shift(other._can_shift) + , _memory_flush(other._memory_flush) + , _seed(other._seed) { +} + +Llama::~Llama() { + if (_sampler) { + llama_sampler_free(_sampler); + } + if (_ctx) { + llama_free(_ctx); + } + if (_model) { + llama_model_free(_model); + } + llama_backend_free(); +} + +void Llama::reset() { + _stop_sequences.clear(); + _last_error.clear(); + _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; + _min_p = 0.0f; + _max_tokens = 150; + _n_system_tokens = 0; + _seed = LLAMA_DEFAULT_SEED; + _sampler_dirty = true; + if (_ctx) { + llama_memory_clear(llama_get_memory(_ctx), true); + } +} + +bool Llama::is_memory_flush() { + auto result = _memory_flush; + if (result) { + _memory_flush = false; + } + return result; +} + +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(); + if (n_gpu_layers >= 0) { + 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) { + set_last_error("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_ENABLED; + + // or Q4_0 for more aggressive saving + cparams.type_k = GGML_TYPE_Q4_0; + cparams.type_v = GGML_TYPE_Q4_0; + + // keep KV cache on GPU + cparams.offload_kqv = true; + + _ctx = llama_init_from_model(_model, cparams); + if (!_ctx) { + set_last_error("Create context"); + } else { + _vocab = llama_model_get_vocab(_model); + _template = llama_model_chat_template(_model, nullptr); + _is_gemma4 = (_template.find("<|turn>model") != string::npos); + _can_shift = llama_memory_can_shift(llama_get_memory(_ctx)); + } + } + + return _last_error.empty(); +} + +bool Llama::load_embedding_model(string model_path) { + ggml_backend_load_all(); + + llama_model_params mparams = llama_model_default_params(); + mparams.n_gpu_layers = 99; + + _last_error.clear(); + _model = llama_model_load_from_file(model_path.c_str(), mparams); + if (!_model) { + set_last_error("Load model"); + } else { + 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"); + } else { + _vocab = llama_model_get_vocab(_model); + } + } + + return _last_error.empty(); +} + +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); + int32_t n = 0; + + if (_template.empty()) { + set_last_error("No chat template available"); + return false; + } + + if (_is_gemma4) { + // 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) { + set_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); + + if (_sampler_dirty) { + // avoid wasteful rebuild + if (!configure_sampler()) { + return false; + } + _sampler_dirty = false; + } + + vector prompt_tokens = tokenize(prompt); + if (prompt_tokens.size() == 0) { + return false; + } + + if (role == "system") { + // always retain system tokens + _n_system_tokens = prompt_tokens.size(); + } + + if (!make_space_for_tokens((prompt_tokens.size() * 3) / 2)) { + return false; + } + + // batch decode tokens + if (!batch_decode_tokens(prompt_tokens)) { + 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); + } + + llama_batch decoder_batch = llama_batch_get_one(&decoder_start_token_id, 1); + if (llama_decode(_ctx, decoder_batch)) { + set_last_error("Failed to evaluate decoder start token"); + return false; + } + } + + iter._tokens_generated = 0; + iter._t_start = std::chrono::high_resolution_clock::now(); + iter._llama = this; + iter._has_next = true; + return true; +} + +string Llama::next(LlamaIter &iter) { + if (!iter._has_next) { + set_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); + if (llama_decode(_ctx, batch)) { + set_last_error("Failed to evaluate token during generation"); + return ""; + } + + 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)) { + 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)) { + set_last_error("Failed to evaluate token during generation"); + break; + } + } + + // tokens exhausted - call add_message to continue + iter._has_next = false; + + // detokenize sequentially + if (!decoded.empty()) { + for (llama_token tok : decoded) { + out.append(token_to_string(iter, tok)); + } + } + + return out; +} + +float Llama::memory_kv_percent() { + 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); + int kv_used = (pos_max < 0) ? 0 : (int)pos_max + 1; + return 100.0f * kv_used / n_ctx; +} + +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 + auto n_gpu_layers = std::max(0, _n_gpu_layers); + info.n_layers_total = llama_model_n_layer(_model); + 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 + if (read_vram(info.vram_used, info.vram_total)) { + 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. "; + } 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. "; + } + 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; +} + +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) { + set_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) { + set_last_error("no embedding returned"); + 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) { + 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 == 1) { + // KV full or fragmented mid-batch - evict oldest tokens and retry + if (!make_space_for_tokens(n_batch)) { + set_decode_error(result, i, tokens.size()); + return false; + } + result = llama_decode(_ctx, batch); + if (result == 1) { + // Eviction reported enough logical space but decode still failed - + // this is fragmentation, not a real space shortage. No defrag API + // is available, so fall back to a full non-system flush, which + // guarantees one contiguous block. + if (!full_flush_except_system()) { + set_decode_error(result, i, tokens.size()); + return false; + } + _memory_flush = true; + result = llama_decode(_ctx, batch); + } + } + if (result != 0) { + set_decode_error(result, i, tokens.size()); + 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) { + set_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::full_flush_except_system() { + llama_memory_t mem = llama_get_memory(_ctx); + llama_pos pos_min = llama_memory_seq_pos_min(mem, 0); + if (pos_min < 0) { + return true; // already empty + } + llama_pos flush_start = pos_min + _n_system_tokens; + bool ok = llama_memory_seq_rm(mem, 0, flush_start, -1); + if (!ok) { + set_last_error("Failed to flush memory past system tokens"); + return false; + } + return true; +} + +// 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 +// +bool Llama::make_space_for_tokens(int n_tokens) { + int n_ctx = llama_n_ctx(_ctx); + if (n_tokens > n_ctx) { + set_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 _n_system_tokens) + int removable = current_used - _n_system_tokens; + if (tokens_to_remove > removable) { + set_last_error("Can't make enough space while keeping num_system_tokens tokens"); + return false; + } + if (!_can_shift) { + set_last_error("Memory type doesn't support shifting, can't evict mid-sequence"); + return false; + } + + llama_pos remove_start = pos_min + _n_system_tokens; + + // Remove oldest tokens (from pos_min to pos_min + tokens_to_remove) + llama_memory_seq_rm(mem, 0, remove_start, remove_start + tokens_to_remove); + + // Shift remaining tokens down + llama_memory_seq_add(mem, 0, remove_start + tokens_to_remove, -1, -tokens_to_remove); + + set_last_error(std::format("made space for {} tokens", n_tokens)); + 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) { + set_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) { + set_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 - 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; + } + } + + 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) { + 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 string &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); + } +} + +void Llama::set_decode_error(int32_t error, int index, int num_tokens) { + if (error == 1) { + llama_memory_t mem = llama_get_memory(_ctx); + llama_pos pos_min = llama_memory_seq_pos_min(mem, 0); + llama_pos pos_max = llama_memory_seq_pos_max(mem, 0); + int n_ctx = llama_n_ctx(_ctx); + int current_used = pos_max - pos_min + 1; + int space_needed = num_tokens; + int space_available = n_ctx - current_used; + _n_system_tokens; + set_last_error(std::format("KV exhausted. Reduce batch or context sizes. batchNo:{} requested:{} available:{}", + index, space_needed, space_available)); + } else { + auto message = error == 2 ? "abort" : error == -1 ? "invalid" : "fatal"; + set_last_error(std::format("Failed to decode batch. batchNo:{} error:'{}'", index, message)); + } +} diff --git a/llama/llama-sb.h b/llama/llama-sb.h new file mode 100644 index 0000000..8306e19 --- /dev/null +++ b/llama/llama-sb.h @@ -0,0 +1,156 @@ +// 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 +#include +#include "llama.h" + +using namespace std; + +struct Llama; +struct RagDB; +struct RagSession; + +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 + int model_native_max_ctx; + + // Advice + string advice; +}; + +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; + int _repetition_count; + int _tokens_generated; + bool _has_next; +}; + +struct Llama { + explicit Llama(); + + // move constructor + Llama(Llama &&other) noexcept; + + // delete the copy + Llama(const Llama &) = delete; + Llama &operator=(const Llama &) = delete; + + ~Llama(); + + // init + 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); + string next(LlamaIter &iter); + string all(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; 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; dirty(); } + + // error handling + const char *last_error() { return _last_error.c_str(); } + void set_log_level(int level) { _log_level = level; } + void reset(); + bool is_memory_flush(); + + // memory info + LlamaMemoryInfo memory_info(); + float memory_kv_percent(); + + // 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(); + void dirty() {_sampler_dirty = true; } + bool full_flush_except_system(); + 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 string &message); + void set_decode_error(int32_t error, int index, int num_tokens); + + llama_model *_model; + llama_context *_ctx; + llama_sampler *_sampler; + const llama_vocab *_vocab; + vector _stop_sequences; + string _grammar_src; + string _grammar_root; + string _last_error; + string _template; + int32_t _penalty_last_n; + float _penalty_repeat; + float _penalty_freq; + float _penalty_present; + float _temperature; + float _top_p; + float _min_p; + int _top_k; + int _max_tokens; + int _log_level; + int _n_gpu_layers; + int _n_system_tokens; + bool _is_gemma4; + bool _sampler_dirty; + bool _can_shift; + bool _memory_flush; + unsigned int _seed; +}; diff --git a/llama/llama.cpp b/llama/llama.cpp new file mode 160000 index 0000000..86b9470 --- /dev/null +++ b/llama/llama.cpp @@ -0,0 +1 @@ +Subproject commit 86b94708f22478f900b76ca02e316f4f3418faff diff --git a/llama/main.cpp b/llama/main.cpp new file mode 100644 index 0000000..b78d015 --- /dev/null +++ b/llama/main.cpp @@ -0,0 +1,597 @@ +// 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 "robin-hood-hashing/src/include/robin_hood.h" +#include "include/log.h" +#include "include/var.h" +#include "include/module.h" +#include "include/param.h" + +#include "llama-sb.h" + +#define CLASS_ID_LLAMA 1 +#define CLASS_ID_LLAMA_ITER 2 + +int g_nextId = 1; +robin_hood::unordered_map g_llama; +robin_hood::unordered_map g_llama_iter; + +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_llama.find(id) != g_llama.end()) { + result = id; + } + } + if (result == -1) { + error(retval, "Llama not found"); + } + 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] == '~') { + const char *home = getenv("HOME"); + if (home != nullptr) { + result.append(home); + result.append(path + 1); + } else { + result = path; + } + } else { + result = 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) +// +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_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_repeat(value); + v_setreal(map_add_var(self, "penalty_repeat", 0), value); + result = 1; + } + } + 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) +// +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_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_last_n(value); + v_setreal(map_add_var(self, "penalty_last_n", 0), value); + result = 1; + } + } + 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_llama_class_id(self, retval); + if (id != -1) { + Llama &llama = g_llama.at(id); + 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; + } + } + 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_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_min_p(value); + v_setreal(map_add_var(self, "min_p", 0), value); + 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_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_temperature(value); + v_setreal(map_add_var(self, "temperature", 0), value); + result = 1; + } + } + return result; +} + +// +// 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; + if (argc != 1) { + error(retval, "llama.set_top_k", 1, 1); + } else { + int id = get_llama_class_id(self, retval); + if (id != -1) { + Llama &llama = g_llama.at(id); + 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; + } + } + 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) { + error(retval, "llama.set_top_p", 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_top_p(value); + v_setreal(map_add_var(self, "top_p", 0), value); + result = 1; + } + } + 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); + 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; + } + } + 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); + auto value = get_param_num(argc, arg, 0, 0); + llama.set_seed(value); + v_setreal(map_add_var(self, "seed", 0), value); + result = 1; + } + } + return result; +} + +// +// llama.reset() - make the model forget everything +// +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_llama_class_id(self, retval); + if (id != -1) { + Llama &llama = g_llama.at(id); + llama.reset(); + result = 1; + } + } + 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() +// +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; +} + +// +// 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; + } + } + return result; +} + +// +// iter.tokens_sec +// +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, "iter.tokens_sec", 0, 0); + } else { + int id = get_llama_iter_class_id(self, retval); + if (id != -1) { + 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; + } + } + return result; +} + +// +// print llama.add_message("please generate as simple program in BASIC to draw a cat") +// +static int cmd_llama_add_message(var_s *self, int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + 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 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); + 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); + result = 1; + } else { + g_llama_iter.erase(iter_id); + error(retval, llama.last_error()); + } + } + } + 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, "")); + 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.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); + 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); + 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); + v_create_callback(retval, "mem_info", cmd_llama_mem_info); + result = 1; + } else { + error(retval, llama.last_error()); + g_llama.erase(id); + result = 0; + } + return result; +} + +FUNC_SIG lib_func[] = { + {1, 5, "LLAMA", cmd_create_llama}, +}; + +SBLIB_API int sblib_func_count() { + return 1; +} + +FUNC_SIG lib_proc[] = {}; + +SBLIB_API int sblib_proc_count() { + return 0; +} + +// +// Program startup +// +int sblib_init(const char *sourceFile) { + return 1; +} + +// +// Release variables falling out of scope +// +SBLIB_API int sblib_free(int cls_id, int id) { + if (id != -1) { + switch (cls_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; + } + } + 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 +// +void sblib_close(void) { + if (!g_llama.empty()) { + fprintf(stderr, "LLM leak detected\n"); + g_llama.clear(); + } + if (!g_llama_iter.empty()) { + fprintf(stderr, "LLM iter leak detected\n"); + g_llama_iter.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/nitro.cpp b/llama/nitro.cpp new file mode 100644 index 0000000..2f64dbd --- /dev/null +++ b/llama/nitro.cpp @@ -0,0 +1,2836 @@ +// nitro.cpp — Nitro Agent +// A standalone agentic LLM shell with notcurses TUI. +// Uses llama-sb.h as the sole llama.cpp integration layer. +// +// 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 (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) +// /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:CURL +// +// Copyright (C) 2026 Chris Warren-Smith — GPLv2 or later +// + +#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" + +#include + +namespace fs = std::filesystem; + +// +// NitroConfig +// +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; + 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; + 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; +}; + +// +// 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 = static_cast(history_stack.size()); + return; + } + history_stack.push_back(input); + current_index = static_cast(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 >= static_cast(history_stack.size())) { + current_index = static_cast(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 = static_cast(history_stack.size()); + } + + /** + * @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) { + 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 = static_cast(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, 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"; + else f << c; + } + f << '\n'; + } + } + + private: + std::vector history_stack; + 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; + int kv_percent = 0; + 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); + void update_usage(int tokens_sec, const LlamaMemoryInfo &mem); + + // ── lifecycle ───────────────────────────────────────────────────── + void init(); + void destroy(); + void resize(); + // ── draw ────────────────────────────────────────────────────────── + void redraw_header() const; + void redraw_chat(); + 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) const; + // 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") const; + // Legacy alias kept for callers that used the old name. + std::string rag_folder_picker(const std::string &start_dir) const { + 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 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); + bool setup_embed(const std::string &path, TuiState &tui); + bool setup_model(const NitroConfig &cfg, TuiState &tui); + void apply_generation_params(const NitroConfig &cfg) const; + void reset_conversation(const std::string &sysprompt, TuiState &tui); + std::string memory_info_status() const; + std::string memory_info_text() const; + 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) const; + std::string restart(const NitroConfig &cfg, TuiState &tui); + float tokens_per_sec() const; +}; + +// +// 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/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); + // 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 +// +static const std::vector CODE_EXTENSIONS = { + ".py",".c",".cpp",".h",".bas",".java",".html",".js",".ts", + ".json",".yaml",".toml",".sh",".go",".rs",".jsx",".tsx" +}; + +// +// Settings persistence (~/.config/nitro/nitro.settings.json) +// 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("nitro.config.json")) { + return "nitro.config.json"; + } + const char *home = getenv("HOME"); + std::string base = home ? std::string(home) : "."; + return base + "/.config/nitro/settings.json"; +} + +// 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.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) { + 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, + 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(); + + cfg.thinking = true; + cfg.agent_id = AgentSessionId::uniqueId(); + + // 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, "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); +} + +// +// 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" + " \"model_path\": \"{}\",\n" + " \"embed_path\": \"{}\",\n" + " \"sandbox\": \"{}\",\n" + " \"n_ctx\": {},\n" + " \"n_batch\": {},\n" + " \"n_gpu_layers\": {},\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.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(); + 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 << introspect(cfg); + + return f.good(); +} + +// +// Trims whitespace from both ends of a string +// +static std::string trim(std::string_view str) { + 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); + 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)); +} + +/* + * 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); + } + } + } + + // Fallback: plain <...> with no matching HTML tags — unwrap the angle brackets + return input.substr(left + 1, right - left - 1); + } + + return input.substr(left, right - left + 1); +} + +// ─── 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 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); +} + +// +// 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 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 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 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 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 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 +// +static std::string build_system_prompt(const 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: " + cfg.sandbox + "\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" + "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" + " 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" + " 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" + " TOOL:RESTART restart after writing current task context to `SESSION.md`\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" + + "## Auto-Restart Protocol\n" + "**When:** - When KV >= 80% (as reported in the tool results footer).\n" + "**Steps:**\n" + "1. **Save State:** Write current task context to `SESSION.md` using `TOOL:WRITE`.\n" + " - Include: Timestamp, KV usage, current task description, pending actions, and last conversation summary.\n" + " - Don't check if SESSION.md already exists from another session. just use TOOL:WRITE.\n" + "2. **Trigger Restart:** Call `TOOL:RESTART`.\n" + "**Example `SESSION.md` Content:**\n" + "```markdown\n" + "# Session State Snapshot\n" + "**Timestamp:**