diff --git a/activate.sh b/activate.sh new file mode 100755 --- /dev/null +++ b/activate.sh @@ -0,0 +1,1 @@ +. ~/Projects/esp-idf-test/esp-idf/export.sh diff --git a/components/esp-osc/.github/workflows/test.yml b/components/esp-osc/.github/workflows/test.yml new file mode 100644 --- /dev/null +++ b/components/esp-osc/.github/workflows/test.yml @@ -0,0 +1,16 @@ +on: [push, pull_request] +name: Test +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + submodules: recursive + - name: Prepare + run: sudo apt-get install python-is-python3 gperf -y + - name: Install + run: make prepare install + - name: Build + run: make build diff --git a/components/esp-osc/.gitignore b/components/esp-osc/.gitignore new file mode 100644 --- /dev/null +++ b/components/esp-osc/.gitignore @@ -0,0 +1,5 @@ +.idea/ +cmake-build-debug/ +test/esp-idf +test/tools +test/build diff --git a/components/esp-osc/CMakeLists.txt b/components/esp-osc/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/components/esp-osc/CMakeLists.txt @@ -0,0 +1,8 @@ +set(srcs + "esp_osc.c" + "tinyosc.c" +) + +idf_component_register(SRCS ${srcs} + INCLUDE_DIRS "." +) diff --git a/components/esp-osc/Kconfig b/components/esp-osc/Kconfig new file mode 100644 diff --git a/components/esp-osc/LICENSE.md b/components/esp-osc/LICENSE.md new file mode 100644 --- /dev/null +++ b/components/esp-osc/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Joël Gähwiler + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/components/esp-osc/Makefile b/components/esp-osc/Makefile new file mode 100644 --- /dev/null +++ b/components/esp-osc/Makefile @@ -0,0 +1,47 @@ +SHELL := /bin/bash + +ESP_IDF_VERSION := "v5.2.2" + +fmt: + clang-format -i ./*.c ./*.h -style="{BasedOnStyle: Google, ColumnLimit: 120, SortIncludes: false}" + clang-format -i ./test/main/*.c -style="{BasedOnStyle: Google, ColumnLimit: 120, SortIncludes: false}" + +prepare: + git clone --recursive https://github.com/espressif/esp-idf.git test/esp-idf + cd test/esp-idf; git fetch; git checkout $(ESP_IDF_VERSION) + cd test/esp-idf/; git submodule update --recursive --init + +update: + cd test/esp-idf; git fetch; git checkout $(ESP_IDF_VERSION) + cd test/esp-idf/; git submodule update --recursive --init + +install: + export IDF_TOOLS_PATH=$(shell pwd)/test/tools; cd test/esp-idf; ./install.sh esp32 + +config: + export IDF_TOOLS_PATH=$(shell pwd)/test/tools; . test/esp-idf/export.sh; cd test; idf.py menuconfig + +reconfigure: + export IDF_TOOLS_PATH=$(shell pwd)/test/tools; . test/esp-idf/export.sh; cd test; idf.py reconfigure + export IDF_TOOLS_PATH=$(shell pwd)/test/tools; . test/esp-idf/export.sh; cd test; idf.py fullclean + +erase: + export IDF_TOOLS_PATH=$(shell pwd)/test/tools; . test/esp-idf/export.sh; cd test; idf.py erase-flash + +clean: + export IDF_TOOLS_PATH=$(shell pwd)/test/tools; . test/esp-idf/export.sh; cd test; idf.py clean + +build: + export IDF_TOOLS_PATH=$(shell pwd)/test/tools; . test/esp-idf/export.sh; cd test; idf.py build + +flash: + export IDF_TOOLS_PATH=$(shell pwd)/test/tools; . test/esp-idf/export.sh; cd test; idf.py flash + +monitor: + export IDF_TOOLS_PATH=$(shell pwd)/test/tools; . test/esp-idf/export.sh; cd test; idf.py monitor + +simple-monitor: + @clear + miniterm.py /dev/cu.SLAB_USBtoUART 115200 --rts 0 --dtr 0 --raw --exit-char 99 + +run: build flash monitor diff --git a/components/esp-osc/README.md b/components/esp-osc/README.md new file mode 100644 --- /dev/null +++ b/components/esp-osc/README.md @@ -0,0 +1,21 @@ +# esp-osc + +[![Test](https://github.com/256dpi/esp-osc/actions/workflows/test.yml/badge.svg)](https://github.com/256dpi/esp-osc/actions/workflows/test.yml) +[![Release](https://img.shields.io/github/release/256dpi/esp-osc.svg)](https://github.com/256dpi/esp-osc/releases) + +**OSC component for esp-idf projects based on the [tinyosc](https://github.com/mhroth/tinyosc) library** + +This component bundles the tinyosc library and provides a simple API for sending OSC messages. + +## Installation + +You can install the component by adding it as a git submodule: + +```bash +git submodule add https://github.com/256dpi/esp-osc.git components/esp-osc +git submodule update --init --recursive +``` + +## Example + +An example can be found here: https://github.com/256dpi/esp-osc/blob/master/test/main/main.c. diff --git a/components/esp-osc/esp_osc.c b/components/esp-osc/esp_osc.c new file mode 100644 --- /dev/null +++ b/components/esp-osc/esp_osc.c @@ -0,0 +1,156 @@ +#include +#include +#include + +#include "esp_osc.h" + +#define TAG "esp-osc" + +bool esp_osc_init(esp_osc_client_t *client, uint16_t buf_len, uint16_t port) { + // free existing memory + if (client->sbuf != NULL) { + free(client->sbuf); + } + if (client->rbuf != NULL) { + free(client->rbuf); + } + + // allocate memory + client->sbuf = malloc(buf_len); + client->rbuf = malloc(buf_len); + client->len = buf_len; + + // close existing socket + if (client->socket != 0) { + close(client->socket); + client->socket = 0; + } + + // create socket + client->socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); + if (client->socket <= 0) { + ESP_LOGE(TAG, "failed to create socket (%d)", errno); + client->socket = 0; + return false; + } + + // bind socket if port is available + if (port > 0) { + struct sockaddr_in addr = {0}; + addr.sin_addr.s_addr = inet_addr("0.0.0.0"); + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + if (bind(client->socket, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + ESP_LOGE(TAG, "failed to bind socket (%d)", errno); + return false; + } + } + + return true; +} + +esp_osc_target_t esp_osc_target(const char *address, uint16_t port) { + // prepare target + esp_osc_target_t target = {0}; + target.addr.sin_addr.s_addr = inet_addr(address); + target.addr.sin_family = AF_INET; + target.addr.sin_port = htons(port); + + return target; +} + +bool esp_osc_send(esp_osc_client_t *client, esp_osc_target_t *target, const char *topic, const char *format, ...) { + // send message + va_list args; + va_start(args, format); + bool ret = esp_osc_send_v(client, target, topic, format, args); + va_end(args); + + return ret; +} + +bool esp_osc_send_v(esp_osc_client_t *client, esp_osc_target_t *target, const char *topic, const char *format, + va_list args) { + // prepare message + uint32_t length = tosc_vwrite((char *)client->sbuf, client->len, topic, format, args); + + // send message + if (sendto(client->socket, client->sbuf, length, 0, (struct sockaddr *)&target->addr, sizeof(target->addr)) < 0) { + ESP_LOGE(TAG, "failed to send message (%d)", errno); + return false; + } + + return true; +} + +bool esp_osc_receive(esp_osc_client_t *client, esp_osc_callback_t callback) { + // prepare values + esp_osc_value_t values[32]; + + for (;;) { + // receive message + ssize_t ret = recvfrom(client->socket, client->rbuf, client->len, 0, NULL, NULL); + if (ret < 0) { + ESP_LOGE(TAG, "failed to receive message (%d)", errno); + return false; + } else if (ret > client->len) { + ESP_LOGE(TAG, "discard too long message (%d)", ret); + return false; + } + + // check bundle + if (tosc_isBundle(client->rbuf)) { + ESP_LOGE(TAG, "discard unsupported bundle"); + return false; + } + + // parse message + tosc_message msg = {0}; + int res = tosc_parseMessage(&msg, client->rbuf, client->len); + if (res < 0) { + ESP_LOGE(TAG, "failed to parse message (%d)", res); + return false; + } + + // get format + char *fmt = tosc_getFormat(&msg); + + // get size + size_t size = strlen(fmt); + if (size > sizeof(values)) { + ESP_LOGE(TAG, "message has too many values (%d)", size); + return false; + } + + // parse values + for (size_t i = 0; i < size; i++) { + switch (fmt[i]) { + case 'i': + values[i].i = tosc_getNextInt32(&msg); + break; + case 'h': + values[i].h = tosc_getNextInt64(&msg); + break; + case 'f': + values[i].f = tosc_getNextFloat(&msg); + break; + case 'd': + values[i].d = tosc_getNextDouble(&msg); + break; + case 's': + values[i].s = tosc_getNextString(&msg); + break; + case 'b': + tosc_getNextBlob(&msg, &values[i].b, &values[i].bl); + break; + } + } + + // call callback + if (!callback(tosc_getAddress(&msg), fmt, values)) { + return false; + } + } + + return true; +} diff --git a/components/esp-osc/esp_osc.h b/components/esp-osc/esp_osc.h new file mode 100644 --- /dev/null +++ b/components/esp-osc/esp_osc.h @@ -0,0 +1,52 @@ +#ifndef ESP_OSC_H +#define ESP_OSC_H + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + char *sbuf; + char *rbuf; + size_t len; + int socket; +} esp_osc_client_t; + +typedef struct { + struct sockaddr_in addr; +} esp_osc_target_t; + +typedef struct { + union { + int32_t i; + int64_t h; + float f; + double d; + const char *s; + const char *b; + }; + int bl; +} esp_osc_value_t; + +typedef bool (*esp_osc_callback_t)(const char *topic, const char *format, esp_osc_value_t *values); + +bool esp_osc_init(esp_osc_client_t *client, uint16_t buf_len, uint16_t port); + +esp_osc_target_t esp_osc_target(const char *address, uint16_t port); + +bool esp_osc_send(esp_osc_client_t *client, esp_osc_target_t *target, const char *topic, const char *format, ...); +bool esp_osc_send_v(esp_osc_client_t *client, esp_osc_target_t *target, const char *topic, const char *format, + va_list args); + +bool esp_osc_receive(esp_osc_client_t *client, esp_osc_callback_t callback); + +#ifdef __cplusplus +} +#endif + +#endif // ESP_OSC_H diff --git a/components/esp-osc/test/CMakeLists.txt b/components/esp-osc/test/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/components/esp-osc/test/CMakeLists.txt @@ -0,0 +1,3 @@ +cmake_minimum_required(VERSION 3.5) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(esp-osc) diff --git a/components/esp-osc/test/components/esp-osc b/components/esp-osc/test/components/esp-osc new file mode 120000 --- /dev/null +++ b/components/esp-osc/test/components/esp-osc @@ -0,0 +1,1 @@ +../../ \ No newline at end of file diff --git a/components/esp-osc/test/main/CMakeLists.txt b/components/esp-osc/test/main/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/components/esp-osc/test/main/CMakeLists.txt @@ -0,0 +1,1 @@ +idf_component_register(SRCS "main.c" INCLUDE_DIRS "") diff --git a/components/esp-osc/test/main/main.c b/components/esp-osc/test/main/main.c new file mode 100644 --- /dev/null +++ b/components/esp-osc/test/main/main.c @@ -0,0 +1,150 @@ +#include + +#include +#include +#include +#include + +#include + +#define WIFI_SSID "" +#define WIFI_PASS "" + +#define OSC_ADDRESS "" +#define OSC_PORT 0 + +#define TAG "main" + +esp_osc_client_t client; + +static void sender() { + // select targets + esp_osc_target_t targets[2] = { + esp_osc_target("127.0.0.1", 9000), + esp_osc_target(OSC_ADDRESS, OSC_PORT), + }; + + for (;;) { + // delay + vTaskDelay(1000 / portTICK_PERIOD_MS); + + // send messages + for (size_t i = 0; i < 2; i++) { + esp_osc_send(&client, &targets[i], "test", "ihfdsb", 42, (int64_t)84, 3.14f, 6.28, "foo", 3, "bar"); + } + } +} + +static bool callback(const char *topic, const char *format, esp_osc_value_t *values) { + // log message + ESP_LOGI(TAG, "got message: %s (%s)", topic, format); + for (size_t i = 0; i < strlen(format); i++) { + switch (format[i]) { + case 'i': + ESP_LOGI(TAG, "==> i: %ld", values[i].i); + break; + case 'h': + ESP_LOGI(TAG, "==> h: %lld", values[i].h); + break; + case 'f': + ESP_LOGI(TAG, "==> f: %f", values[i].f); + break; + case 'd': + ESP_LOGI(TAG, "==> d: %f", values[i].d); + break; + case 's': + ESP_LOGI(TAG, "==> s: %s", values[i].s); + break; + case 'b': + ESP_LOGI(TAG, "==> b: %.*s (%d)", values[i].bl, values[i].b, values[i].bl); + break; + } + } + + return true; +} + +static void receiver() { + for (;;) { + // receive messages + esp_osc_receive(&client, callback); + } +} + +static void restarter() { + for (;;) { + // delay + vTaskDelay(5000 / portTICK_PERIOD_MS); + + // restart client + esp_osc_init(&client, 1024, 9000); + } +} + +static void handler(void *arg, esp_event_base_t base, int32_t id, void *data) { + if (base == WIFI_EVENT) { + switch (id) { + case WIFI_EVENT_STA_START: + // connect to ap + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_connect()); + + break; + + case IP_EVENT_STA_GOT_IP: + break; + + case WIFI_EVENT_STA_DISCONNECTED: + // reconnect Wi-Fi + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_connect()); + + break; + + default: + break; + } + } +} + +void app_main() { + // initialize NVS flash + ESP_ERROR_CHECK(nvs_flash_init()); + + // initialize networking + ESP_ERROR_CHECK(esp_netif_init()); + + // create default event loop + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + // enable Wi-Fi + esp_netif_create_default_wifi_sta(); + + // initialize Wi-Fi + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + // set Wi-Fi storage to ram + ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM)); + + // set wifi mode + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + + // register event handlers + ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &handler, NULL, NULL)); + ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &handler, NULL, NULL)); + + // prepare Wi-Fi config + wifi_config_t wifi_config = {.sta = {.ssid = WIFI_SSID, .password = WIFI_PASS}}; + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config)); + + // start Wi-Fi + ESP_ERROR_CHECK(esp_wifi_start()); + + // prepare client + esp_osc_init(&client, 1024, 9000); + + // create tasks + xTaskCreatePinnedToCore(sender, "sender", 4096, NULL, 10, NULL, 1); + xTaskCreatePinnedToCore(receiver, "receiver", 4096, NULL, 10, NULL, 1); + xTaskCreatePinnedToCore(restarter, "restarter", 4096, NULL, 10, NULL, 1); +} diff --git a/components/esp-osc/tinyosc.c b/components/esp-osc/tinyosc.c new file mode 100644 --- /dev/null +++ b/components/esp-osc/tinyosc.c @@ -0,0 +1,331 @@ +/** + * Copyright (c) 2015-2018, Martin Roth (mhroth@gmail.com) + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include +#if _WIN32 +#include +#define tosc_strncpy(_dst, _src, _len) strncpy_s(_dst, _len, _src, _TRUNCATE) +#else +#include +#define tosc_strncpy(_dst, _src, _len) strncpy(_dst, _src, _len) +#endif +#if __unix__ && !__APPLE__ +#include +#define htonll(x) htobe64(x) +#define ntohll(x) be64toh(x) +#endif +#include "tinyosc.h" + +#define BUNDLE_ID 0x2362756E646C6500L // "#bundle" + +// http://opensoundcontrol.org/spec-1_0 +int tosc_parseMessage(tosc_message *o, char *buffer, const int len) { + // NOTE(mhroth): if there's a comma in the address, that's weird + int i = 0; + while (buffer[i] != '\0') ++i; // find the null-terimated address + while (buffer[i] != ',') ++i; // find the comma which starts the format string + if (i >= len) return -1; // error while looking for format string + // format string is null terminated + o->format = buffer + i + 1; // format starts after comma + + while (i < len && buffer[i] != '\0') ++i; + if (i == len) return -2; // format string not null terminated + + i = (i + 4) & ~0x3; // advance to the next multiple of 4 after trailing '\0' + o->marker = buffer + i; + + o->buffer = buffer; + o->len = len; + + return 0; +} + +// check if first eight bytes are '#bundle ' +bool tosc_isBundle(const char *buffer) { return ((*(const int64_t *)buffer) == htonll(BUNDLE_ID)); } + +void tosc_parseBundle(tosc_bundle *b, char *buffer, const int len) { + b->buffer = (char *)buffer; + b->marker = buffer + 16; // move past '#bundle ' and timetag fields + b->bufLen = len; + b->bundleLen = len; +} + +uint64_t tosc_getTimetag(tosc_bundle *b) { return ntohll(*((uint64_t *)(b->buffer + 8))); } + +uint32_t tosc_getBundleLength(tosc_bundle *b) { return b->bundleLen; } + +bool tosc_getNextMessage(tosc_bundle *b, tosc_message *o) { + if ((b->marker - b->buffer) >= b->bundleLen) return false; + uint32_t len = (uint32_t)ntohl(*((int32_t *)b->marker)); + tosc_parseMessage(o, b->marker + 4, len); + b->marker += (4 + len); // move marker to next bundle element + return true; +} + +char *tosc_getAddress(tosc_message *o) { return o->buffer; } + +char *tosc_getFormat(tosc_message *o) { return o->format; } + +uint32_t tosc_getLength(tosc_message *o) { return o->len; } + +int32_t tosc_getNextInt32(tosc_message *o) { + // convert from big-endian (network btye order) + const int32_t i = (int32_t)ntohl(*((uint32_t *)o->marker)); + o->marker += 4; + return i; +} + +int64_t tosc_getNextInt64(tosc_message *o) { + const int64_t i = (int64_t)ntohll(*((uint64_t *)o->marker)); + o->marker += 8; + return i; +} + +uint64_t tosc_getNextTimetag(tosc_message *o) { return (uint64_t)tosc_getNextInt64(o); } + +float tosc_getNextFloat(tosc_message *o) { + // convert from big-endian (network btye order) + const uint32_t i = ntohl(*((uint32_t *)o->marker)); + o->marker += 4; + return *((float *)(&i)); +} + +double tosc_getNextDouble(tosc_message *o) { + const uint64_t i = ntohll(*((uint64_t *)o->marker)); + o->marker += 8; + return *((double *)(&i)); +} + +const char *tosc_getNextString(tosc_message *o) { + int i = (int)strlen(o->marker); + if (o->marker + i >= o->buffer + o->len) return NULL; + const char *s = o->marker; + i = (i + 4) & ~0x3; // advance to next multiple of 4 after trailing '\0' + o->marker += i; + return s; +} + +void tosc_getNextBlob(tosc_message *o, const char **buffer, int *len) { + int i = (int)ntohl(*((uint32_t *)o->marker)); // get the blob length + if (o->marker + 4 + i <= o->buffer + o->len) { + *len = i; // length of blob + *buffer = o->marker + 4; + i = (i + 7) & ~0x3; + o->marker += i; + } else { + *len = 0; + *buffer = NULL; + } +} + +unsigned char *tosc_getNextMidi(tosc_message *o) { + unsigned char *m = (unsigned char *)o->marker; + o->marker += 4; + return m; +} + +tosc_message *tosc_reset(tosc_message *o) { + int i = 0; + while (o->format[i] != '\0') ++i; + i = (i + 4) & ~0x3; // advance to the next multiple of 4 after trailing '\0' + o->marker = o->format + i - 1; // -1 to account for ',' format prefix + return o; +} + +void tosc_writeBundle(tosc_bundle *b, uint64_t timetag, char *buffer, const int len) { + *((uint64_t *)buffer) = htonll(BUNDLE_ID); + *((uint64_t *)(buffer + 8)) = htonll(timetag); + + b->buffer = buffer; + b->marker = buffer + 16; + b->bufLen = len; + b->bundleLen = 16; +} + +// always writes a multiple of 4 bytes +uint32_t tosc_vwrite(char *buffer, const int len, const char *address, const char *format, va_list ap) { + memset(buffer, 0, len); // clear the buffer + uint32_t i = (uint32_t)strlen(address); + if (address == NULL || i >= len) return -1; + tosc_strncpy(buffer, address, len); + i = (i + 4) & ~0x3; + buffer[i++] = ','; + int s_len = (int)strlen(format); + if (format == NULL || (i + s_len) >= len) return -2; + tosc_strncpy(buffer + i, format, len - i - s_len); + i = (i + 4 + s_len) & ~0x3; + + for (int j = 0; format[j] != '\0'; ++j) { + switch (format[j]) { + case 'b': { + const uint32_t n = (uint32_t)va_arg(ap, int); // length of blob + if (i + 4 + n > len) return -3; + char *b = (char *)va_arg(ap, void *); // pointer to binary data + *((uint32_t *)(buffer + i)) = htonl(n); + i += 4; + memcpy(buffer + i, b, n); + i = (i + 3 + n) & ~0x3; + break; + } + case 'f': { + if (i + 4 > len) return -3; + const float f = (float)va_arg(ap, double); + *((uint32_t *)(buffer + i)) = htonl(*((uint32_t *)&f)); + i += 4; + break; + } + case 'd': { + if (i + 8 > len) return -3; + const double f = (double)va_arg(ap, double); + *((uint64_t *)(buffer + i)) = htonll(*((uint64_t *)&f)); + i += 8; + break; + } + case 'i': { + if (i + 4 > len) return -3; + const uint32_t k = (uint32_t)va_arg(ap, int); + *((uint32_t *)(buffer + i)) = htonl(k); + i += 4; + break; + } + case 'm': { + if (i + 4 > len) return -3; + const unsigned char *const k = (unsigned char *)va_arg(ap, void *); + memcpy(buffer + i, k, 4); + i += 4; + break; + } + case 't': + case 'h': { + if (i + 8 > len) return -3; + const uint64_t k = (uint64_t)va_arg(ap, long long); + *((uint64_t *)(buffer + i)) = htonll(k); + i += 8; + break; + } + case 's': { + const char *str = (const char *)va_arg(ap, void *); + s_len = (int)strlen(str); + if (i + s_len >= len) return -3; + tosc_strncpy(buffer + i, str, len - i - s_len); + i = (i + 4 + s_len) & ~0x3; + break; + } + case 'T': // true + case 'F': // false + case 'N': // nil + case 'I': // infinitum + break; + default: + return -4; // unknown type + } + } + + return i; // return the total number of bytes written +} + +uint32_t tosc_writeNextMessage(tosc_bundle *b, const char *address, const char *format, ...) { + va_list ap; + va_start(ap, format); + if (b->bundleLen >= b->bufLen) return 0; + const uint32_t i = tosc_vwrite(b->marker + 4, b->bufLen - b->bundleLen - 4, address, format, ap); + va_end(ap); + *((uint32_t *)b->marker) = htonl(i); // write the length of the message + b->marker += (4 + i); + b->bundleLen += (4 + i); + return i; +} + +uint32_t tosc_writeMessage(char *buffer, const int len, const char *address, const char *format, ...) { + va_list ap; + va_start(ap, format); + const uint32_t i = tosc_vwrite(buffer, len, address, format, ap); + va_end(ap); + return i; // return the total number of bytes written +} + +void tosc_printOscBuffer(char *buffer, const int len) { + // parse the buffer contents (the raw OSC bytes) + // a return value of 0 indicates no error + tosc_message m; + const int err = tosc_parseMessage(&m, buffer, len); + if (err == 0) + tosc_printMessage(&m); + else + printf("Error while reading OSC buffer: %i\n", err); +} + +void tosc_printMessage(tosc_message *osc) { + printf("[%lu bytes] %s %s", + osc->len, // the number of bytes in the OSC message + tosc_getAddress(osc), // the OSC address string, e.g. "/button1" + tosc_getFormat(osc)); // the OSC format string, e.g. "f" + + for (int i = 0; osc->format[i] != '\0'; i++) { + switch (osc->format[i]) { + case 'b': { + const char *b = NULL; // will point to binary data + int n = 0; // takes the length of the blob + tosc_getNextBlob(osc, &b, &n); + printf(" [%i]", n); // print length of blob + for (int j = 0; j < n; ++j) printf("%02X", b[j] & 0xFF); // print blob bytes + break; + } + case 'm': { + unsigned char *m = tosc_getNextMidi(osc); + printf(" 0x%02X%02X%02X%02X", m[0], m[1], m[2], m[3]); + break; + } + case 'f': + printf(" %g", tosc_getNextFloat(osc)); + break; + case 'd': + printf(" %g", tosc_getNextDouble(osc)); + break; + case 'i': + printf(" %ld", tosc_getNextInt32(osc)); + break; + case 'h': + printf(" %lld", tosc_getNextInt64(osc)); + break; + case 't': + printf(" %lld", tosc_getNextTimetag(osc)); + break; + case 's': + printf(" %s", tosc_getNextString(osc)); + break; + case 'F': + printf(" false"); + break; + case 'I': + printf(" inf"); + break; + case 'N': + printf(" nil"); + break; + case 'T': + printf(" true"); + break; + default: + printf(" Unknown format: '%c'", osc->format[i]); + break; + } + } + printf("\n"); +} diff --git a/components/esp-osc/tinyosc.h b/components/esp-osc/tinyosc.h new file mode 100644 --- /dev/null +++ b/components/esp-osc/tinyosc.h @@ -0,0 +1,193 @@ +/** + * Copyright (c) 2015-2018, Martin Roth (mhroth@gmail.com) + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef _TINY_OSC_ +#define _TINY_OSC_ + +#include +#include + +#define TINYOSC_TIMETAG_IMMEDIATELY 1L + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef os_bswap_64 +#define os_bswap_64(x) \ + ((uint64_t)((((x)&0xff00000000000000ull) >> 56) | (((x)&0x00ff000000000000ull) >> 40) | \ + (((x)&0x0000ff0000000000ull) >> 24) | (((x)&0x000000ff00000000ull) >> 8) | \ + (((x)&0x00000000ff000000ull) << 8) | (((x)&0x0000000000ff0000ull) << 24) | \ + (((x)&0x000000000000ff00ull) << 40) | (((x)&0x00000000000000ffull) << 56))) +#endif + +#ifndef htonll +#define htonll(x) os_bswap_64(x) +#endif + +#ifndef ntohll +#define ntohll htonll +#endif + +typedef struct tosc_message { + char *format; // a pointer to the format field + char *marker; // the current read head + char *buffer; // the original message data (also points to the address) + uint32_t len; // length of the buffer data +} tosc_message; + +typedef struct tosc_bundle { + char *marker; // the current write head (where the next message will be written) + char *buffer; // the original buffer + uint32_t bufLen; // the byte length of the original buffer + uint32_t bundleLen; // the byte length of the total bundle +} tosc_bundle; + +/** + * Returns true if the buffer refers to a bundle of OSC messages. False otherwise. + */ +bool tosc_isBundle(const char *buffer); + +/** + * Reads a buffer containing a bundle of OSC messages. + */ +void tosc_parseBundle(tosc_bundle *b, char *buffer, const int len); + +/** + * Returns the timetag of an OSC bundle. + */ +uint64_t tosc_getTimetag(tosc_bundle *b); + +/** + * Parses the next message in a bundle. Returns true if successful. + * False otherwise. + */ +bool tosc_getNextMessage(tosc_bundle *b, tosc_message *o); + +/** + * Returns a point to the address block of the OSC buffer. + * This is also the start of the buffer. + */ +char *tosc_getAddress(tosc_message *o); + +/** + * Returns a pointer to the format block of the OSC buffer. + */ +char *tosc_getFormat(tosc_message *o); + +/** + * Returns the length in bytes of this message. + */ +uint32_t tosc_getLength(tosc_message *o); + +/** + * Returns the next 32-bit int. Does not check buffer bounds. + */ +int32_t tosc_getNextInt32(tosc_message *o); + +/** + * Returns the next 64-bit int. Does not check buffer bounds. + */ +int64_t tosc_getNextInt64(tosc_message *o); + +/** + * Returns the next 64-bit timetag. Does not check buffer bounds. + */ +uint64_t tosc_getNextTimetag(tosc_message *o); + +/** + * Returns the next 32-bit float. Does not check buffer bounds. + */ +float tosc_getNextFloat(tosc_message *o); + +/** + * Returns the next 64-bit float. Does not check buffer bounds. + */ +double tosc_getNextDouble(tosc_message *o); + +/** + * Returns the next string, or NULL if the buffer length is exceeded. + */ +const char *tosc_getNextString(tosc_message *o); + +/** + * Points the given buffer pointer to the next blob. + * The len pointer is set to the length of the blob. + * Returns NULL and 0 if the OSC buffer bounds are exceeded. + */ +void tosc_getNextBlob(tosc_message *o, const char **buffer, int *len); + +/** + * Returns the next set of midi bytes. Does not check bounds. + * Bytes from MSB to LSB are: port id, status byte, data1, data2. + */ +unsigned char *tosc_getNextMidi(tosc_message *o); + +/** + * Resets the read head to the first element. + * + * @return The same tosc_message pointer. + */ +tosc_message *tosc_reset(tosc_message *o); + +/** + * Parse a buffer containing an OSC message. + * The contents of the buffer are NOT copied. + * The tosc_message struct only points at relevant parts of the original buffer. + * Returns 0 if there is no error. An error code (a negative number) otherwise. + */ +int tosc_parseMessage(tosc_message *o, char *buffer, const int len); + +/** + * Starts writing a bundle to the given buffer with length. + */ +void tosc_writeBundle(tosc_bundle *b, uint64_t timetag, char *buffer, const int len); + +/** + * Write a message to a bundle buffer. Returns the number of bytes written. + */ +uint32_t tosc_writeNextMessage(tosc_bundle *b, const char *address, const char *format, ...); + +/** + * Returns the length in bytes of the bundle. + */ +uint32_t tosc_getBundleLength(tosc_bundle *b); + +/** + * Writes an OSC packet to a buffer. Returns the total number of bytes written. + * The entire buffer is cleared before writing. + */ +uint32_t tosc_writeMessage(char *buffer, const int len, const char *address, const char *fmt, ...); + +/** + * A convenience function to (non-destructively) print a buffer containing + * an OSC message to stdout. + */ +void tosc_printOscBuffer(char *buffer, const int len); + +/** + * A convenience function to (non-destructively) print a pre-parsed OSC message + * to stdout. + */ +void tosc_printMessage(tosc_message *o); + +uint32_t tosc_vwrite(char *buffer, const int len, const char *address, const char *format, va_list ap); + +#ifdef __cplusplus +} +#endif + +#endif // _TINY_OSC_ diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,2 +1,2 @@ -idf_component_register(SRCS "main.c" "wifi.c" "usb_cdc.c" "can.c" "display.c" "display_gui.c" +idf_component_register(SRCS "main.c" "wifi.c" "usb_cdc.c" "can.c" "display.c" "display_gui.c" "osc_control.c" INCLUDE_DIRS .) diff --git a/main/display_gui.c b/main/display_gui.c --- a/main/display_gui.c +++ b/main/display_gui.c @@ -41,13 +41,23 @@ void display_gui_homescreen(void) display_lock(0); label = lv_label_create(lv_scr_act()); - lv_label_set_text(label, "Yup"); + lv_label_set_text(label, "Boot Complete"); // lv_obj_align_to(label, lv_scr_act(), LV_ALIGN_OUT_BOTTOM_MID, 0, 10); + // 14 + // 20 + // 24 + // 28 + // montserrat + + + // lv_obj_set_style_local_text_font(label, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_FONT_MONTSERRAT_28); + lv_obj_set_style_text_font(label, &lv_font_montserrat_28, LV_PART_MAIN| LV_STATE_DEFAULT); + // display_slider(); //Call one at a time to see examples - display_meter(); + //display_meter(); // display_image(); // display_window(); //display_dropdown(); @@ -56,6 +66,24 @@ void display_gui_homescreen(void) } +// TODO: make a display task that listens for messages from a queue that change visual elements +uint32_t value = 0; +void display_gui_process(void) +{ + + display_lock(0); + + //set_value(meter_indic1, value); + + //value = (value + 1) % 100; + + display_unlock(); + +} + + + + @@ -236,23 +264,6 @@ void display_update_text(char* str) -// make a display task that listens for messages from a queue that change visual elements -uint32_t value = 0; - -void display_gui_process(void) -{ - - display_lock(0); - - set_value(meter_indic1, value); - - value = (value + 1) % 100; - - display_unlock(); - -} - - diff --git a/main/main.c b/main/main.c --- a/main/main.c +++ b/main/main.c @@ -11,7 +11,7 @@ #include "esp_log.h" #include "nvs_flash.h" #include "sdkconfig.h" - +#include "osc_control.h" #include "wifi.h" // #include "usb_cdc.h" #include "can.h" @@ -47,6 +47,9 @@ void app_main(void) // Connect to wifi wifi_init(); + // Initialize OSC + osc_init(); + // Initialize canbus //can_init(); diff --git a/main/osc_control.c b/main/osc_control.c new file mode 100644 --- /dev/null +++ b/main/osc_control.c @@ -0,0 +1,100 @@ +#include "osc_control.h" +#include +#include +#include +#include +#include "display.h" +#include + +#define WIFI_SSID "" +#define WIFI_PASS "" + +#define OSC_ADDRESS "" +#define OSC_PORT 0 + +static const char *TAG = "osc_control"; + +esp_osc_client_t client; + +static void sender() { + // select targets + esp_osc_target_t targets[2] = { + esp_osc_target("127.0.0.1", 9000), + esp_osc_target(OSC_ADDRESS, OSC_PORT), + }; + + for (;;) { + // delay + vTaskDelay(1000 / portTICK_PERIOD_MS); + + // send messages + for (size_t i = 0; i < 2; i++) { + esp_osc_send(&client, &targets[i], "test", "ihfdsb", 42, (int64_t)84, 3.14f, 6.28, "foo", 3, "bar"); + } + } +} + +static bool callback(const char *topic, const char *format, esp_osc_value_t *values) { + // log message + ESP_LOGI(TAG, "got message: %s (%s)", topic, format); + + + char out[512] = {0}; + snprintf(out, 128, "topic: %s\nformat: %s\n", topic, format); + + for (size_t i = 0; i < strlen(format); i++) { + switch (format[i]) { + case 'i': + ESP_LOGI(TAG, "==> i: %ld", values[i].i); + break; + case 'h': + ESP_LOGI(TAG, "==> h: %lld", values[i].h); + break; + case 'f': + snprintf(out+strlen(out), 128-strlen(out), "Value: %f", values[i].f); + ESP_LOGI(TAG, "==> f: %f", values[i].f); + break; + case 'd': + ESP_LOGI(TAG, "==> d: %f", values[i].d); + break; + case 's': + ESP_LOGI(TAG, "==> s: %s", values[i].s); + break; + case 'b': + ESP_LOGI(TAG, "==> b: %.*s (%d)", values[i].bl, values[i].b, values[i].bl); + break; + } + } + + display_update_text(out); + + return true; +} + +static void receiver() { + for (;;) { + // receive messages + esp_osc_receive(&client, callback); + } +} + +static void restarter() { + for (;;) { + // delay + vTaskDelay(5000 / portTICK_PERIOD_MS); + + // restart client + esp_osc_init(&client, 1024, 9000); + } +} + + +void osc_init() { + // prepare client + esp_osc_init(&client, 1024, 5005); + + // create tasks + //xTaskCreatePinnedToCore(sender, "sender", 4096, NULL, 10, NULL, 1); + xTaskCreatePinnedToCore(receiver, "receiver", 4096, NULL, 10, NULL, 1); + //xTaskCreatePinnedToCore(restarter, "restarter", 4096, NULL, 10, NULL, 1); +} \ No newline at end of file diff --git a/main/osc_control.h b/main/osc_control.h new file mode 100644 --- /dev/null +++ b/main/osc_control.h @@ -0,0 +1,9 @@ + +#ifndef _OSC_CONTROL_H_ +#define _OSC_CONTROL_H_ + + +void osc_init(); + + +#endif // _OSC_CONTROL_H_ diff --git a/main/wifi.c b/main/wifi.c --- a/main/wifi.c +++ b/main/wifi.c @@ -33,8 +33,8 @@ static void __event_handler(void* arg, e { esp_wifi_connect(); s_retry_num++; - ESP_LOGI(TAG, "retry to connect to the AP"); - //display_update_text("AP Connect Retry"); + ESP_LOGI(TAG, "retry to connect to the AP"); + display_update_text("AP Connect Retry"); } else @@ -42,7 +42,7 @@ static void __event_handler(void* arg, e xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT); } ESP_LOGI(TAG,"connect to the AP fail"); - //display_update_text("AP Connect Fail"); + display_update_text("AP Connect Fail"); } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) @@ -50,7 +50,7 @@ static void __event_handler(void* arg, e ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data; ESP_LOGI(TAG, "WIFI OK, IP: " IPSTR, IP2STR(&event->ip_info.ip)); char out[128] = {0}; - snprintf(out, 128, "WIFI OK, IP: " IPSTR, IP2STR(&event->ip_info.ip)); + snprintf(out, 128, "WIFI OK\nIP: " IPSTR, IP2STR(&event->ip_info.ip)); display_update_text(out);