├── .github
└── workflows
│ └── deploy-docs.yml
├── .gitignore
├── .gitmodules
├── .python-version
├── LICENSE
├── benchmark
├── bela2python2bela-benchmark.py
├── bela2python2bela-benchmark
│ ├── Watcher.cpp
│ ├── Watcher.h
│ ├── config.h
│ └── render.cpp
├── data-processing.ipynb
├── readme.md
└── run-benchmark.sh
├── dev
├── test-dist.sh
└── test-docs.sh
├── docs
├── _static
│ └── custom.css
├── conf.py
├── docs-readme.md
├── index.rst
└── modules.rst
├── pybela
├── Controller.py
├── Logger.py
├── Monitor.py
├── Streamer.py
├── Watcher.py
├── __init__.py
└── utils.py
├── pyproject.toml
├── readme.md
├── requirements.txt
├── test
├── __init__.py
├── bela-test-send
│ ├── Watcher.cpp
│ ├── Watcher.h
│ └── render.cpp
├── bela-test
│ ├── Watcher.cpp
│ ├── Watcher.h
│ ├── render.cpp
│ └── sketch.js
├── readme.md
├── test.py
└── test_send.py
├── tutorials
├── bela-code
│ ├── bela2python2bela
│ │ ├── Watcher.cpp
│ │ ├── Watcher.h
│ │ └── render.cpp
│ ├── potentiometers
│ │ ├── Watcher.cpp
│ │ ├── Watcher.h
│ │ └── render.cpp
│ └── timestamping
│ │ ├── Watcher.cpp
│ │ ├── Watcher.h
│ │ └── render.cpp
└── notebooks
│ ├── 1_Streamer-Bela-to-python-basics.ipynb
│ ├── 2_Streamer-Bela-to-python-advanced.ipynb
│ ├── 3_Streamer-python-to-Bela.ipynb
│ ├── 4_Monitor.ipynb
│ ├── 5_Logger.ipynb
│ ├── 6_Controller.ipynb
│ ├── 7_Sparse-timestamping.ipynb
│ └── potentiometers-circuit.png
└── uv.lock
/.github/workflows/deploy-docs.yml:
--------------------------------------------------------------------------------
1 | name: Deploy Documentation to GitHub Pages
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: Checkout repository
14 | uses: actions/checkout@v2
15 |
16 | - name: Set up Python
17 | uses: actions/setup-python@v2
18 | with:
19 | python-version: "3.10"
20 |
21 | - name: Install dependencies
22 | run: |
23 | python -m pip install --upgrade pip
24 | pip install uv
25 | uv venv
26 | uv pip install '.[dev]'
27 | sudo apt-get update && sudo apt-get install -y pandoc
28 |
29 | - name: Build documentation
30 | run: |
31 | pandoc -s readme.md -o docs/readme.rst
32 | uv run sphinx-build -M html docs/ docs/_build
33 |
34 | - name: Deploy to GitHub Pages
35 | uses: peaceiris/actions-gh-pages@v3
36 | with:
37 | github_token: ${{ secrets.GITHUB_TOKEN }}
38 | publish_dir: ./docs/_build/html
39 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .ipynb_checkpoints
3 | */test-env/*
4 | *.err
5 | .vscode/
6 | __pycache__/
7 | build/
8 | *.egg-info
9 | *.bin
10 | *.fzz
11 | dist/
12 | .clang-format
13 | docs/_build/*
14 | docs/readme.rst
15 | deptree.txt
16 | .venv/*
17 | *.log
18 | *.csv
19 | benchmark/data/*
20 | .pep8
21 | *.tex
22 | dev.*
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "watcher"]
2 | path = watcher
3 | url = https://github.com/BelaPlatform/watcher.git
4 |
--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------
1 | 3.10
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
--------------------------------------------------------------------------------
/benchmark/bela2python2bela-benchmark.py:
--------------------------------------------------------------------------------
1 | from pybela import Streamer
2 | import numpy as np
3 | import csv
4 | import argparse
5 |
6 | BUFFER_LENGTH = 1024
7 | TIME_INTERVAL = 30
8 | NUM_AUX_VARIABLES = 1
9 |
10 |
11 | async def callback(buffer, streamer):
12 | """when the streamer receives a buffer, it calls this function and passes the buffer as an argument"""
13 | # diff frames elapsed from previous buffer
14 | _var = buffer['name']
15 | diffFramesElapsed = buffer['buffer']['data'][0]
16 | diffs[_var].append(diffFramesElapsed)
17 |
18 | ref_timestamp = buffer['buffer']['ref_timestamp']
19 | frames[_var].append(ref_timestamp)
20 |
21 | buffer_id, buffer_type = vars.index(_var), 'i'
22 | data_list = np.zeros(BUFFER_LENGTH, dtype=int)
23 | data_list[0] = ref_timestamp
24 | streamer.send_buffer(buffer_id, buffer_type, BUFFER_LENGTH, data_list)
25 |
26 |
27 | def save_to_csv(var_to_save, filename):
28 | with open(filename, 'w', newline='') as f:
29 | writer = csv.writer(f)
30 |
31 | writer.writerow(vars)
32 |
33 | max_len = max(len(var_to_save[_var]) for _var in vars)
34 | for i in range(max_len):
35 | row = []
36 | for _var in vars:
37 | # Add timestamp and diff if available, otherwise add empty values
38 | if i < len(var_to_save[_var]):
39 | row.extend([var_to_save[_var][i]])
40 | else:
41 | row.extend([""]) # Fill with empty strings if data is missing
42 | writer.writerow(row)
43 |
44 |
45 | if __name__ == "__main__":
46 |
47 | # argument parsing
48 | parser = argparse.ArgumentParser()
49 | parser.add_argument("--rfn", type=str, default="", help="root file name")
50 | parser.add_argument("--time", type=int, default=30, help="time interval in seconds")
51 | parser.add_argument("--numAuxVars", type=int, default=1, help="number of aux variables")
52 |
53 | args = parser.parse_args()
54 | root_filename = args.rfn
55 | TIME_INTERVAL = args.time
56 | NUM_AUX_VARIABLES = args.numAuxVars
57 |
58 | streamer = Streamer()
59 | streamer.connect()
60 | streamer.connect_ssh()
61 |
62 | vars = [f'auxWatcherVar{idx}' for idx in range(NUM_AUX_VARIABLES)]
63 | diffs, frames = {var: [] for var in vars}, {var: [] for var in vars}
64 |
65 | cpu_logs_bela_path = f"/root/Bela/projects/{streamer.project_name}/cpu-logs"
66 | cpu_logs_filename = f"{root_filename}_cpu-load.log"
67 |
68 | # < -- streaming starts -- >
69 |
70 | streamer.start_streaming(vars, on_buffer_callback=callback, callback_args=(streamer))
71 |
72 | # start cpu monitoring
73 | print("Starting Bela-CPU monitoring...")
74 |
75 | # truncate cpu logs file
76 | streamer.ssh_client.exec_command(f"echo '' > {cpu_logs_bela_path}/{cpu_logs_filename}")
77 |
78 | # watcher is ticking inside binaryDataCallback so we need to send data to get it started
79 | buffer_type, _zeros = 'i', np.zeros(BUFFER_LENGTH, dtype=int)
80 | for idx in range(NUM_AUX_VARIABLES):
81 | streamer.send_buffer(idx, buffer_type, BUFFER_LENGTH, _zeros) # send zeros buffer to get it started
82 |
83 | # stream for n seconds
84 | streamer.wait(TIME_INTERVAL)
85 |
86 | streamer.stop_streaming()
87 |
88 | # < -- streaming finishes -- >
89 |
90 | diffs_in_ms = {}
91 | sr = streamer.sample_rate
92 | for _var in vars:
93 | # calc diff in ms for each var
94 | # drop first value (it is the time it takes to receive the first buffer), use np.array to allow element-wise operations
95 | diffs[_var], frames[_var] = np.array(diffs[_var][1:]), np.array(frames[_var][1:])
96 | diffs_in_ms[_var] = np.round(diffs[_var]*1000/sr, 1) # 1 dec. position
97 |
98 | # print average roundtrip for each var
99 | avg_roundtrip = np.round(np.average(diffs_in_ms[_var]), 2) # discard first value
100 | print(f"{_var} -- average roundtrip {avg_roundtrip} ms ; num of buffers received: {len(diffs[_var])}")
101 |
102 | # save diffs to csv
103 | save_to_csv(diffs_in_ms, f"benchmark/data/{root_filename}_diffs.csv")
104 | save_to_csv(frames, f"benchmark/data/{root_filename}_frames.csv")
105 |
--------------------------------------------------------------------------------
/benchmark/bela2python2bela-benchmark/Watcher.cpp:
--------------------------------------------------------------------------------
1 | ../../watcher/Watcher.cpp
--------------------------------------------------------------------------------
/benchmark/bela2python2bela-benchmark/Watcher.h:
--------------------------------------------------------------------------------
1 | ../../watcher/Watcher.h
--------------------------------------------------------------------------------
/benchmark/bela2python2bela-benchmark/config.h:
--------------------------------------------------------------------------------
1 | #ifndef CONFIG_H
2 | #define CONFIG_H
3 |
4 | #define NUM_AUX_VARIABLES 10
5 | #define NUM_OSCS 40
6 | #define VERBOSE 0
7 |
8 | #endif // CONFIG_H
9 |
--------------------------------------------------------------------------------
/benchmark/bela2python2bela-benchmark/render.cpp:
--------------------------------------------------------------------------------
1 | // make -C /root/Bela PROJECT=bela2python2bela-benchmark CPPFLAGS="-DNUM_AUX_VARIABLES=1" run
2 | #include "config.h" // defines VERBOSE, NUM_AUX_VARIABLES, NUM_OSCS
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 |
9 | std::vector*> auxWatcherVars;
10 |
11 | struct ReceivedBuffer {
12 | uint32_t bufferId;
13 | char bufferType[4];
14 | uint32_t bufferLen;
15 | uint32_t empty;
16 | std::vector bufferData;
17 | };
18 | ReceivedBuffer receivedBuffer;
19 | uint receivedBufferHeaderSize;
20 | uint receivedBufferLen = 1024; // size of the buffer to be received from python
21 | uint64_t receivedBuffersCount;
22 |
23 | uint gFramesElapsed = 0;
24 |
25 | // osc bank variables
26 | float gPhaseIncrement;
27 | float gFrequencies[NUM_OSCS];
28 | float gPhases[NUM_OSCS];
29 | float gFrequenciesLFO[NUM_OSCS];
30 | float gPhasesLFO[NUM_OSCS];
31 | float gScale;
32 |
33 | bool binaryDataCallback(const std::string& addr, const WSServerDetails* id, const unsigned char* data, size_t size, void* arg) {
34 |
35 | uint _framesElapsed = gFramesElapsed; // copy the value of frameselapsed so that it does not vary inside the function
36 |
37 | receivedBuffersCount++;
38 |
39 | // parse received buffer
40 | std::memcpy(&receivedBuffer, data, receivedBufferHeaderSize);
41 | std::memcpy(receivedBuffer.bufferData.data(), data + receivedBufferHeaderSize, receivedBuffer.bufferLen * sizeof(int)); // data is a pointer to the beginning of the data
42 |
43 | int _diffFramesElapsed;
44 | if (receivedBuffer.bufferData[0] == 0) {
45 | _diffFramesElapsed = 0;
46 | } else {
47 | _diffFramesElapsed = _framesElapsed - receivedBuffer.bufferData[0];
48 | }
49 |
50 | // assign the watched variable and tick the watcher 1024 times to fill the buffer that is sent to python
51 | uint32_t _id = receivedBuffer.bufferId;
52 | for (size_t i = 0; i < receivedBuffer.bufferLen; ++i) {
53 | Bela_getDefaultWatcherManager()->tick(_framesElapsed); // tick needs to happen before assignment
54 | *auxWatcherVars[_id] = _diffFramesElapsed;
55 | }
56 |
57 | if (VERBOSE) {
58 | printf("\ntotal received count: %llu, total data size: %zu, bufferId: %d, bufferType: %s, bufferLen: %d\n", receivedBuffersCount, size, receivedBuffer.bufferId, receivedBuffer.bufferType,
59 | receivedBuffer.bufferLen);
60 |
61 | printf("diff frames elapsed: %zu, _framesElapsed: %d, receivedFramesElapsed: %zu \n", auxWatcherVars[_id]->get(), _framesElapsed, receivedBuffer.bufferData[0]);
62 | }
63 |
64 | return true;
65 | }
66 |
67 | bool setup(BelaContext* context, void* userData) {
68 |
69 | printf("NUM_AUX_VARIABLES: %zu\n", NUM_AUX_VARIABLES);
70 | printf("NUM_OSCS: %zu\n", NUM_OSCS);
71 |
72 | // auxWatcherVars needs to be defined before configuring WatcherManager
73 | auxWatcherVars.resize(NUM_AUX_VARIABLES);
74 | for (unsigned int i = 0; i < NUM_AUX_VARIABLES; ++i) {
75 | auxWatcherVars[i] = new Watcher("auxWatcherVar" + std::to_string(i));
76 | }
77 |
78 | // set up Watcher Manager
79 | Bela_getDefaultWatcherManager()->getGui().setup(context->projectName);
80 | Bela_getDefaultWatcherManager()->setup(context->audioSampleRate); // set sample rate in watcher
81 |
82 | // set up Data Receiver
83 | for (unsigned int i = 0; i < NUM_AUX_VARIABLES; ++i) {
84 | Bela_getDefaultWatcherManager()->getGui().setBuffer('i', receivedBufferLen);
85 | }
86 | Bela_getDefaultWatcherManager()->getGui().setBinaryDataCallback(binaryDataCallback);
87 |
88 | receivedBuffer.bufferLen = receivedBufferLen;
89 | receivedBufferHeaderSize = sizeof(receivedBuffer.bufferId) + sizeof(receivedBuffer.bufferType) + sizeof(receivedBuffer.bufferLen) + sizeof(receivedBuffer.empty);
90 | receivedBuffer.bufferData.resize(receivedBufferLen);
91 |
92 | receivedBuffersCount = 0;
93 |
94 | // oscillator bank (to increase CPU usage)
95 |
96 | if (NUM_OSCS > 0) {
97 | gPhaseIncrement = 2.0 * M_PI * 1.0 / context->audioSampleRate;
98 | gScale = 1 / (float)NUM_OSCS * 0.5;
99 |
100 | srand(time(NULL));
101 |
102 | for (int k = 0; k < NUM_OSCS; ++k) {
103 | // Fill array gFrequencies[k] with random freq between 300 - 2700Hz
104 | gFrequencies[k] = rand() / (float)RAND_MAX * 2400 + 300;
105 | // Fill array gFrequenciesLFO[k] with random freq between 0.001 - 0.051Hz
106 | gFrequenciesLFO[k] = rand() / (float)RAND_MAX * 0.05 + 0.001;
107 | gPhasesLFO[k] = 0;
108 | }
109 | }
110 | return true;
111 | }
112 |
113 | void render(BelaContext* context, void* userData) {
114 |
115 | for (unsigned int n = 0; n < context->audioFrames; n++) {
116 | gFramesElapsed = context->audioFramesElapsed + n;
117 |
118 | if (NUM_OSCS > 0) {
119 | float out[2] = {0};
120 |
121 | for (int k = 0; k < NUM_OSCS; ++k) {
122 |
123 | // Calculate the LFO amplitude
124 | float LFO = sinf_neon(gPhasesLFO[k]);
125 | gPhasesLFO[k] += gFrequenciesLFO[k] * gPhaseIncrement;
126 | if (gPhasesLFO[k] > M_PI)
127 | gPhasesLFO[k] -= 2.0f * (float)M_PI;
128 |
129 | // Calculate oscillator sinewaves and output them amplitude modulated
130 | // by LFO sinewave squared.
131 | // Outputs from the oscillators are summed in out[],
132 | // with even numbered oscillators going to the left channel out[0]
133 | // and odd numbered oscillators going to the right channel out[1]
134 | out[k & 1] += sinf_neon(gPhases[k]) * gScale * (LFO * LFO);
135 | gPhases[k] += gFrequencies[k] * gPhaseIncrement;
136 | if (gPhases[k] > M_PI)
137 | gPhases[k] -= 2.0f * (float)M_PI;
138 | }
139 | audioWrite(context, n, 0, out[0]);
140 | audioWrite(context, n, 1, out[1]);
141 | }
142 | }
143 | }
144 |
145 | void cleanup(BelaContext* context, void* userData) {
146 | }
147 |
--------------------------------------------------------------------------------
/benchmark/data-processing.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": null,
6 | "id": "8fef6589",
7 | "metadata": {},
8 | "outputs": [],
9 | "source": [
10 | "import os\n",
11 | "import pandas as pd\n",
12 | "import numpy as np"
13 | ]
14 | },
15 | {
16 | "cell_type": "markdown",
17 | "id": "e5d6e31a",
18 | "metadata": {},
19 | "source": [
20 | "# process data and save into csv\n",
21 | "for each measurement configuration (num. of streamed variables, num. of oscillators in filterbank), obtain the average and maximum (worst case) latency, jitter, and average CPU load. "
22 | ]
23 | },
24 | {
25 | "cell_type": "code",
26 | "execution_count": null,
27 | "id": "7eeb6eeb",
28 | "metadata": {},
29 | "outputs": [
30 | {
31 | "name": "stdout",
32 | "output_type": "stream",
33 | "text": [
34 | "USB-V1-O0, mean: 7.6 ms, jitter: 10.1 ms, max: 33.0 ms, cpu_load: 0.37\n",
35 | "USB-V5-O0, mean: 25.0 ms, jitter: 36.2 ms, max: 122.6 ms, cpu_load: 0.68\n",
36 | "USB-V10-O0, mean: 47.8 ms, jitter: 65.9 ms, max: 196.6 ms, cpu_load: 0.67\n",
37 | "USB-V20-O0, mean: 95.9 ms, jitter: 120.1 ms, max: 292.1 ms, cpu_load: 0.66\n",
38 | "USB-V50-O0, mean: 240.2 ms, jitter: 180.9 ms, max: 602.6 ms, cpu_load: 0.67\n",
39 | "USB-V100-O0, mean: 475.9 ms, jitter: 221.7 ms, max: 767.0 ms, cpu_load: 0.67\n",
40 | "USB-V1-O20, mean: 12.3 ms, jitter: 23.7 ms, max: 71.8 ms, cpu_load: 0.76\n",
41 | "USB-V5-O20, mean: 47.7 ms, jitter: 95.1 ms, max: 221.0 ms, cpu_load: 0.9\n",
42 | "USB-V10-O20, mean: 93.6 ms, jitter: 145.7 ms, max: 352.7 ms, cpu_load: 0.9\n",
43 | "USB-V20-O20, mean: 186.7 ms, jitter: 254.6 ms, max: 546.0 ms, cpu_load: 0.89\n",
44 | "USB-V50-O20, mean: 469.1 ms, jitter: 378.1 ms, max: 989.0 ms, cpu_load: 0.9\n",
45 | "USB-V100-O20, mean: 894.9 ms, jitter: 432.6 ms, max: 1347.1 ms, cpu_load: 0.88\n",
46 | "USB-V1-O40, mean: 43.0 ms, jitter: 107.7 ms, max: 195.9 ms, cpu_load: 0.98\n",
47 | "USB-V5-O40, mean: 193.2 ms, jitter: 375.4 ms, max: 775.3 ms, cpu_load: 0.97\n",
48 | "USB-V10-O40, mean: 380.9 ms, jitter: 556.1 ms, max: 998.1 ms, cpu_load: 0.97\n",
49 | "USB-V20-O40, mean: 754.7 ms, jitter: 1194.4 ms, max: 2181.6 ms, cpu_load: 0.98\n",
50 | "USB-V50-O40, mean: 1896.0 ms, jitter: 1413.2 ms, max: 3279.8 ms, cpu_load: 0.98\n",
51 | "USB-V100-O40, mean: 3671.0 ms, jitter: 1745.7 ms, max: 6800.2 ms, cpu_load: 0.98\n"
52 | ]
53 | }
54 | ],
55 | "source": [
56 | "# list files in folder \n",
57 | "num_oscs = [0, 20, 40]\n",
58 | "num_aux_vars = [1, 5, 10, 20, 50, 100]\n",
59 | "configurations = [ [aux_num,osc_num] for osc_num in num_oscs for aux_num in num_aux_vars]\n",
60 | "\n",
61 | "# write header to csv file\n",
62 | "with open(\"data/processed.csv\", \"a\") as f:\n",
63 | " f.write(\"aux_num,osc_num,mean,jitter,max,cpu_load\\n\")\n",
64 | "\n",
65 | "\n",
66 | "for config in configurations:\n",
67 | " \n",
68 | " aux_num, osc_num = config[0], config[1]\n",
69 | " config_array = f\"USB-V{aux_num}-O{osc_num}\"\n",
70 | " \n",
71 | " # retrieve files from data/folder that contain the configuration name\n",
72 | " files = [f for f in os.listdir(\"data\") if config_array in f]\n",
73 | " # get mean cpu load\n",
74 | " cpu_load_file = [f for f in files if \"cpu-load.log\" in f][0]\n",
75 | " cpu_load_df = pd.read_csv(f\"data/{cpu_load_file}\", sep=\" \", header=None).dropna().iloc[:, [1]] # Keep only the second column, which is the cpu_load column \n",
76 | " # drop last six rows (they show measurements after the Bela code is stopped)\n",
77 | " cpu_load_df = cpu_load_df.iloc[:-6]\n",
78 | " cpu_load_array = cpu_load_df.iloc[:, 0].str.rstrip('%').astype(float).to_numpy() / 100\n",
79 | " # cap values >100 to 100\n",
80 | " cpu_load_array[cpu_load_array > 1] = 1\n",
81 | " mean_cpu_load = np.mean(cpu_load_array)\n",
82 | " \n",
83 | " diff_file = [f for f in files if \"diffs.csv\" in f][0]\n",
84 | " diff_df = pd.read_csv(f\"data/{diff_file}\")\n",
85 | "\n",
86 | " # calc jitter as the difference between the 97.5 and 2.5 percentiles of the mean across all variables\n",
87 | " diff_df_mean_across_vars = diff_df.mean(axis=1) \n",
88 | " lower_bound = diff_df_mean_across_vars.quantile(0.025)\n",
89 | " upper_bound = diff_df_mean_across_vars.quantile(0.975)\n",
90 | " jitter = upper_bound - lower_bound\n",
91 | " \n",
92 | " # get mean and max across all variables (not using std as it's not a normal distribution)\n",
93 | " stats = diff_df.describe()\n",
94 | " mean = stats.loc[\"mean\"].mean().round(1) # average across variables\n",
95 | " max = stats.loc[\"max\"].max().round(1)\n",
96 | "\n",
97 | " print(f\"{config_array}, mean: {mean:.1f} ms, jitter: {jitter:.1f} ms, max: {max:.1f} ms, cpu_load: {mean_cpu_load:.2}\")\n",
98 | "\n",
99 | " # store in csv file\n",
100 | " with open(\"data/processed.csv\", \"a\") as f:\n",
101 | " f.write(f\"{aux_num},{osc_num},{mean:.1f},{jitter:.1f},{max:.1f},{mean_cpu_load:.2}\\n\")\n",
102 | " "
103 | ]
104 | },
105 | {
106 | "cell_type": "markdown",
107 | "id": "97d1de21",
108 | "metadata": {},
109 | "source": [
110 | "export to latex"
111 | ]
112 | },
113 | {
114 | "cell_type": "code",
115 | "execution_count": 28,
116 | "id": "3eff39c9",
117 | "metadata": {},
118 | "outputs": [],
119 | "source": [
120 | "df = pd.read_csv(\"data/processed.csv\")\n",
121 | "#sort by aux_num and osc_num\n",
122 | "df = df.sort_values(by=[\"aux_num\", \"osc_num\"])\n",
123 | "\n",
124 | "# export to latex\n",
125 | "with open('table.tex', 'w') as tf:\n",
126 | " tf.write(df.to_latex())"
127 | ]
128 | }
129 | ],
130 | "metadata": {
131 | "kernelspec": {
132 | "display_name": ".venv",
133 | "language": "python",
134 | "name": "python3"
135 | },
136 | "language_info": {
137 | "codemirror_mode": {
138 | "name": "ipython",
139 | "version": 3
140 | },
141 | "file_extension": ".py",
142 | "mimetype": "text/x-python",
143 | "name": "python",
144 | "nbconvert_exporter": "python",
145 | "pygments_lexer": "ipython3",
146 | "version": "3.10.16"
147 | }
148 | },
149 | "nbformat": 4,
150 | "nbformat_minor": 5
151 | }
152 |
--------------------------------------------------------------------------------
/benchmark/readme.md:
--------------------------------------------------------------------------------
1 | # bela2python2bela benchmark
2 |
3 | _tested using Bela `dev` branch commit `63c8089a`_
4 |
5 | To run the benchmarking routine, first connect your Bela board to your computer. Then, open a terminal in this directory and run the following command:
6 |
7 | ```bash
8 | bash run_benchmark.sh
9 | ```
10 |
11 | This will run the benchmark for each configuration and save the results in the `data/` directory. See `data-processing.ipynb` for the data processing code, to obtain, for each configuration, average and maximum latency, jitter, and CPU usage.
12 |
--------------------------------------------------------------------------------
/benchmark/run-benchmark.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | export NUM_AUX_VARIABLES=100
4 | export NUM_OSCS=0
5 | export TIME=60
6 | export TRANSPORT_MODE="USB"
7 | export VERBOSE=0
8 |
9 | for NUM_AUX_VARIABLES in 1 5 10 20 50 100
10 | do
11 |
12 | printf "\n\n==============================="
13 | printf "\n>>Running benchmark for NUM_AUX_VARIABLES=$NUM_AUX_VARIABLES"
14 | printf "\n===============================\n"
15 |
16 | printf ">>Running benchmark with the following parameters:"
17 | printf "\nNUM_AUX_VARIABLES: $NUM_AUX_VARIABLES"
18 | printf "\nNUM_OSCS: $NUM_OSCS"
19 | printf "\nTIME: $TIME"
20 | printf "\nTRANSPORT_MODE: $TRANSPORT_MODE"
21 | printf "\nVERBOSE: $VERBOSE"
22 |
23 | # avoid recompilation with
24 | # bash benchmark/run-benchmark.sh testing
25 | if [ "$1" != "testing" ]; then
26 | printf "\n>> Passing parameters to config.h file..."
27 | cat < benchmark/bela2python2bela-benchmark/config.h
28 | #ifndef CONFIG_H
29 | #define CONFIG_H
30 |
31 | #define NUM_AUX_VARIABLES $NUM_AUX_VARIABLES
32 | #define NUM_OSCS $NUM_OSCS
33 | #define VERBOSE $VERBOSE
34 |
35 | #endif // CONFIG_H
36 | EOL
37 |
38 | printf "\n\n>>Copying benchmark project files to Bela...\n"
39 | rsync -avL benchmark/bela2python2bela-benchmark root@bela.local:Bela/projects/
40 |
41 | printf "\n\n>>Compile the benchmark project on Bela...\n"
42 | ssh root@bela.local 'make -C /root/Bela PROJECT=bela2python2bela-benchmark'
43 | fi
44 |
45 | printf "\n\n>> Starting the node bela-cpu.js process...\n"
46 | timestamp=$(date +%Y%m%d_%H%M%S)
47 | echo $timestamp
48 | cpu_logs_bela_path="/root/Bela/projects/bela2python2bela-benchmark/cpu-logs"
49 | root_filename=$timestamp-${TRANSPORT_MODE}-V${NUM_AUX_VARIABLES}-O${NUM_OSCS}
50 | cpu_logs_filename=$root_filename"_cpu-load.log"
51 |
52 | # kill previous instances
53 | ssh root@bela.local "pkill -f -SIGINT 'bela-cpu.js'"
54 |
55 | # make sure logs paths exists
56 | ssh root@bela.local "mkdir -p ${cpu_logs_bela_path}"
57 |
58 | # start node process
59 | ssh root@bela.local "stdbuf -o0 -i0 -e0 node /root/Bela/IDE/dist/bela-cpu.js > ${cpu_logs_bela_path}/${cpu_logs_filename} 2>&1 &"
60 |
61 | sleep 1 # give some time to node to start
62 |
63 | printf "\n\n>> Run the benchmark project in Bela\n"
64 | ssh root@bela.local 'make -C /root/Bela run PROJECT=bela2python2bela-benchmark' &
65 |
66 | printf "\n\n>>Waiting for Bela to start...\n"
67 | sleep 5
68 |
69 | printf "\n\n>>Running python script...\n"
70 | uv run python benchmark/bela2python2bela-benchmark.py --rfn $root_filename --time $TIME --numAuxVars $NUM_AUX_VARIABLES
71 |
72 | printf "\n\n>>Stop Bela project\n"
73 | ssh root@bela.local 'make -C /root/Bela PROJECT=bela2python2bela-benchmark stop'
74 |
75 | printf "\n\n>>Stop CPU Monitoring...\n"
76 | ssh root@bela.local "pkill -f -SIGINT 'bela-cpu.js'"
77 |
78 | sleep 2
79 |
80 | printf "\n\n>>Copying CPU logs from Bela...\n"
81 | rsync -av root@bela.local:$cpu_logs_bela_path/$cpu_logs_filename benchmark/data/$cpu_logs_filename
82 | # cpu-logs legend: MSW, CPU usage, audio thread CPU usage
83 |
84 | printf "\n\n>>Benchmark complete for NUM_AUX_VARIABLES=$NUM_AUX_VARIABLES\n"
85 | done
--------------------------------------------------------------------------------
/dev/test-dist.sh:
--------------------------------------------------------------------------------
1 | # Check if $1 is passed
2 | if [ -z "$1" ]; then
3 | printf "Error: Pass version. Usage: sh dev.test-dist.sh "
4 | exit 1
5 | fi
6 |
7 | uv run twine check dist/pybela-$1-py3-none-any.whl
8 | uv run twine check dist/pybela-$1.tar.gz
9 |
10 |
11 | cd test
12 |
13 | # remove virtual env if it exists
14 | if [ -d "test-env" ]; then
15 | rm -rf test-env
16 | fi
17 |
18 | printf "\nCreating test-env..."
19 | python -m venv test-env
20 | source test-env/bin/activate
21 | printf "\nInstalling pybela from dist..."
22 | pip install ../dist/pybela-$1-py3-none-any.whl
23 |
24 |
25 | printf "\n>>Copying test project files to Bela...\n"
26 | rsync -avL bela-test root@bela.local:Bela/projects/
27 |
28 | printf "\n>>Compile the project in Bela...\n"
29 | ssh root@bela.local 'make -C /root/Bela PROJECT=bela-test'
30 |
31 | printf "\n>>Run the project in Bela...\n"
32 | ssh root@bela.local 'make -C /root/Bela run PROJECT=bela-test' & # non-blocking
33 |
34 | sleep 2
35 |
36 | printf "\nRunning test.py..."
37 | python test.py
38 | deactivate
39 | rm -rf test-env
--------------------------------------------------------------------------------
/dev/test-docs.sh:
--------------------------------------------------------------------------------
1 | rm -r docs/_build
2 | uv run pandoc -s readme.md -o docs/readme.rst
3 | uv run sphinx-build -M html docs/ docs/_build
4 | open docs/_build/html/index.html
--------------------------------------------------------------------------------
/docs/_static/custom.css:
--------------------------------------------------------------------------------
1 | footer {
2 | display: none !important;
3 | }
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # For the full list of built-in configuration values, see the documentation:
4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
5 |
6 | # -- Project information -----------------------------------------------------
7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
8 |
9 | import os
10 | import sys
11 | import toml
12 |
13 | sys.path.insert(0, os.path.abspath('../pybela'))
14 |
15 | author = 'Teresa Pelinski'
16 | copyright = '2025'
17 |
18 | def get_version_from_pyproject_toml():
19 | with open('../pyproject.toml', 'r') as f:
20 | pyproject_data = toml.load(f)
21 | print(pyproject_data)
22 | return pyproject_data['project']['version']
23 |
24 | release = get_version_from_pyproject_toml()
25 | project = f'pybela {release}'
26 |
27 | # -- General configuration ---------------------------------------------------
28 | extensions = [
29 | 'sphinx.ext.napoleon', 'sphinx.ext.viewcode', 'sphinx_rtd_theme']
30 | templates_path = ['_templates']
31 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', '../pybela/utils.py']
32 |
33 |
34 | # -- Options for HTML output -------------------------------------------------
35 | html_theme = 'sphinx_rtd_theme'
36 | html_css_files = [
37 | 'custom.css',
38 | ]
39 | html_static_path = ['_static']
40 | html_css_files = ['custom.css']
41 | html_show_sphinx = False
42 | html_show_sourcelink = False
43 | html_sidebars = {
44 | '**': ['globaltoc.html', 'searchbox.html']
45 | }
46 | html_theme_options = {
47 | 'collapse_navigation': False,
48 | 'sticky_navigation': True,
49 | 'navigation_depth': 4,
50 | 'titles_only': False,
51 | 'display_version': True,
52 | 'prev_next_buttons_location': 'None',
53 | }
54 |
55 | # remove title from readme file to avoid duplication
56 | file_path = 'readme.rst'
57 |
58 | with open(file_path, 'r+') as file:
59 | lines = file.readlines()[2:] # Read lines and skip the first two
60 | file.seek(0) # Move the cursor to the beginning of the file
61 | file.writelines(lines) # Write the modified lines
62 | file.truncate() # Truncate the file to the new size
63 |
--------------------------------------------------------------------------------
/docs/docs-readme.md:
--------------------------------------------------------------------------------
1 | To build the docs you will need to install `pandoc` to convert the `readme.md` into `rst` (the format used by `sphinx`, the docs builder). You can see the installation instructions [here](https://pandoc.org/installing.html).
2 |
3 | Then you can build the docs with:
4 |
5 | ```bash
6 | rm -r _build
7 | pandoc -s ../readme.md -o readme.rst
8 | uv run sphinx-build -M html . _build
9 | ```
10 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. pybela documentation master file, created by
2 | sphinx-quickstart on Tue Aug 6 18:36:50 2024.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | pybela docs
7 | ===========
8 | Welcome to pybela’s documentation!
9 |
10 | .. include:: readme.rst
11 |
12 | .. toctree::
13 | :maxdepth: 2
14 | :caption: Getting started with pybela
15 | :hidden:
16 |
17 | readme
18 |
19 | .. toctree::
20 | :caption: Module documentation
21 | :maxdepth: 4
22 | :hidden:
23 |
24 | genindex
25 | modules
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/docs/modules.rst:
--------------------------------------------------------------------------------
1 |
2 | .. automodule:: pybela.Watcher
3 | :members:
4 | :undoc-members:
5 | :show-inheritance:
6 |
7 | .. automodule:: pybela.Streamer
8 | :members:
9 | :undoc-members:
10 | :show-inheritance:
11 |
12 | .. automodule:: pybela.Logger
13 | :members:
14 | :undoc-members:
15 | :show-inheritance:
16 |
17 | .. automodule:: pybela.Monitor
18 | :members:
19 | :undoc-members:
20 | :show-inheritance:
21 |
22 | .. automodule:: pybela.Controller
23 | :members:
24 | :undoc-members:
25 | :show-inheritance:
26 |
--------------------------------------------------------------------------------
/pybela/Controller.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from .Watcher import Watcher
3 | from .utils import _print_info, _print_warning
4 |
5 |
6 | class Controller(Watcher):
7 | def __init__(self, ip="192.168.7.2", port=5555, data_add="gui_data", control_add="gui_control"):
8 | """Controller class
9 | Note: All values set with the controller class will be only visible through the "get_value()" method, or the "value" field in the list() function. Values streamed with the streamer, logger or monitor classes will not be affected.
10 |
11 | Args:
12 | ip (str, optional): Remote address IP. If using internet over USB, the IP won't work, pass "bela.local". Defaults to "192.168.7.2".
13 | port (int, optional): Remote address port. Defaults to 5555.
14 | data_add (str, optional): Data endpoint. Defaults to "gui_data".
15 | control_add (str, optional): Control endpoint. Defaults to "gui_control".
16 | """
17 | super(Controller, self).__init__(ip, port, data_add, control_add)
18 |
19 | self._mode = "CONTROL"
20 |
21 | def start_controlling(self, variables=[]):
22 | """Starts controlling given variables. This function will block until all requested variables are set to 'controlled' in the list.
23 | Note: All values set with the controller class will be only visible through the "get_value()" method, or the "value" field in the list() function. Values streamed with the streamer, logger or monitor classes will not be affected.
24 |
25 | Args:
26 | variables (list, optional): List of variables to control. If no variables are specified, stream all watcher variables (default).
27 | """
28 |
29 | variables = self._var_arg_checker(variables)
30 |
31 | self.send_ctrl_msg(
32 | {"watcher": [{"cmd": "control", "watchers": variables}]})
33 |
34 | async def async_wait_for_control_mode_to_be_set(variables=variables):
35 | # wait for variables to be set as 'controlled' in list
36 | _controlled_status = await self._async_get_controlled_status(
37 | variables) # avoid multiple calls to list
38 | while not all([_controlled_status[var] for var in variables]):
39 | await asyncio.sleep(0.2)
40 |
41 | self.loop.run_until_complete(
42 | async_wait_for_control_mode_to_be_set(variables=variables))
43 |
44 | _print_info(
45 | f"Started controlling variables {variables}... Run stop_controlling() to stop controlling the variable values.")
46 |
47 | def stop_controlling(self, variables=[]):
48 | """Stops controlling given variables. This function will block until all requested variables are set to 'uncontrolled' in the list.
49 | Note: All values set with the controller class will be only visible through the "get_value()" method, or the "value" field in the list() function.
50 |
51 | Args:
52 | variables (list, optional): List of variables to control. If no variables are specified, stream all watcher variables (default).
53 | """
54 |
55 | variables = self._var_arg_checker(variables)
56 |
57 | self.send_ctrl_msg(
58 | {"watcher": [{"cmd": "uncontrol", "watchers": variables}]})
59 |
60 | async def async_wait_for_control_mode_to_be_set(variables=variables):
61 | # wait for variables to be set as 'uncontrolled' in list
62 | _controlled_status = await self._async_get_controlled_status(
63 | variables) # avoid multiple calls to list
64 | while all([_controlled_status[var] for var in variables]):
65 | await asyncio.sleep(0.5)
66 |
67 | self.loop.run_until_complete(
68 | async_wait_for_control_mode_to_be_set(variables=variables))
69 |
70 | _print_info(f"Stopped controlling variables {variables}.")
71 |
72 | def send_value(self, variables, values):
73 | """Send a value to the given variables.
74 | Note: All values set with this function will be only visible through the "get_value()" method, or the "value" field in the list() function. Values streamed with the streamer, logger or monitor classes will not be affected.
75 |
76 | Args:
77 | variables (list, required): List of variables to control.
78 | values (list, required): Values to be set for each variable.
79 | """
80 |
81 | assert isinstance(values, list) and len(
82 | values) > 0, "At least one value per variable should be provided."
83 |
84 | variables = self._var_arg_checker(variables)
85 |
86 | assert len(variables) == len(
87 | values), "The number of variables and values should be the same."
88 |
89 | for var in variables:
90 | _type = self.get_prop_of_var(var, "type")
91 |
92 | value = values[variables.index(var)]
93 |
94 | if value % 1 != 0 and _type in ["i", "j"]:
95 | _print_warning(
96 | f"Value {value} is not an integer, but the variable {var} is of type {_type}. Only the integer part will be sent.")
97 |
98 | self.send_ctrl_msg(
99 | {"watcher": [{"cmd": "set", "watchers": variables, "values": values}]})
100 |
101 | async def _async_get_controlled_status(self, variables=[]):
102 | """Async version of get_controller_status"""
103 | variables = self._var_arg_checker(variables)
104 | _list = await self._async_list()
105 | return {var['name']: var['controlled'] for var in _list['watchers'] if var['name'] in variables}
106 |
107 | def get_controlled_status(self, variables=[]):
108 | """Gets the controlled status (controlled or uncontrolled) of the variables
109 |
110 | Args:
111 | variables (list of str, optional): List of variables to check. Defaults to all variables in the watcher.
112 |
113 | Returns:
114 | list of str: List of controlled status of the variables
115 | """
116 | variables = self._var_arg_checker(variables)
117 | return {var['name']: var['controlled'] for var in self.list()['watchers'] if var['name'] in variables}
118 |
119 | def get_value(self, variables=[]):
120 | """ Gets the value of the variables
121 |
122 | Args:
123 | variables (list of str, optional): List of variables to get the value from. Defaults to all variables in the watcher.
124 |
125 | Returns:
126 | list of numbers: List of controlled values of the variables
127 | """
128 | variables = self._var_arg_checker(variables)
129 | return {var['name']: var['value'] for var in self.list()['watchers'] if var['name'] in variables}
130 |
--------------------------------------------------------------------------------
/pybela/Logger.py:
--------------------------------------------------------------------------------
1 | import os
2 | import asyncio
3 | import aiofiles
4 | import struct
5 | from .Watcher import Watcher
6 | from .utils import _print_error, _print_info, _print_ok, _print_warning
7 |
8 |
9 | class Logger(Watcher):
10 | def __init__(self, ip="192.168.7.2", port=5555, data_add="gui_data", control_add="gui_control"):
11 | """ Logger class
12 |
13 | Args:
14 | ip (str, optional): Remote address IP. If using internet over USB, the IP won't work, pass "bela.local". Defaults to "192.168.7.2".
15 | port (int, optional): Remote address port. Defaults to 5555.
16 | data_add (str, optional): Data endpoint. Defaults to "gui_data".
17 | control_add (str, optional): Control endpoint. Defaults to "gui_control".
18 | """
19 | super(Logger, self).__init__(ip, port, data_add, control_add)
20 |
21 | self._logging_mode = "OFF"
22 | self._logging_vars = []
23 | self._logging_transfer = True
24 |
25 | self._active_copying_tasks = []
26 |
27 | self._mode = "LOG"
28 |
29 | # -- logging methods --
30 |
31 | def is_logging(self):
32 | """ Returns True if the logger is currently logging, false otherwise.
33 |
34 | Returns:
35 | bool: Logger status
36 | """
37 | return True if self._logging_mode != "OFF" else False
38 |
39 | def start_logging(self, variables=[], transfer=True, logging_dir="./"):
40 | """ Starts logging session. The session can be ended by calling stop_logging().
41 |
42 | Args:
43 | variables (list of str, optional): List of variables to be logged. If no variables are passed, all variables in the watcher are logged. Defaults to [].
44 | transfer (bool, optional): If True, the logged files will be transferred automatically during the logging session. Defaults to False. #FIXME too slow
45 |
46 | Returns:
47 | list of str: List of local paths to the logged files.
48 | """
49 | remote_paths = self.loop.run_until_complete(self.__async_logging_common_routine(
50 | mode="FOREVER", timestamps=[], durations=[], variables=variables, logging_dir=logging_dir))
51 |
52 | local_paths = {}
53 | if transfer:
54 | self.connect_ssh() # start ssh connection
55 | for var in [v for v in self.watcher_vars if v["name"] in variables]:
56 | var = var["name"]
57 | local_path = os.path.join(
58 | logging_dir, os.path.basename(remote_paths[var]))
59 |
60 | # if file already exists, throw a warning and add number at the end of the filename
61 | local_paths[var] = self._generate_local_filename(
62 | local_path)
63 |
64 | copying_task = self.__copy_file_in_chunks(
65 | remote_paths[var], local_paths[var])
66 | self._active_copying_tasks.append(copying_task)
67 |
68 | return {"local_paths": local_paths, "remote_paths": remote_paths}
69 |
70 | def schedule_logging(self, variables=[], timestamps=[], durations=[], transfer=True, logging_dir="./"):
71 | """Schedule logging session. The session starts at the specified timestamps and lasts for the specified durations. If the timestamp is in the past, the logging will start immediately. The session can be ended by calling stop_logging().
72 |
73 | Args:
74 | variables (list, optional): Variables to be logged. Defaults to [].
75 | timestamps (list, optional): Timestamps to start logging (one for each variable). Defaults to [].
76 | durations (list, optional): Durations to log for (one for each variable). Defaults to [].
77 | transfer (bool, optional): Transfer files to laptop automatically during logging session. Defaults to True.
78 | logging_dir (str, optional): Path to store the files. Defaults to "./".
79 | """
80 |
81 | # check timestamps and duration types
82 | assert isinstance(
83 | timestamps, list) and all(isinstance(timestamp, int) for timestamp in timestamps), "Error: timestamps must be a list of ints."
84 | assert isinstance(
85 | durations, list) and all(isinstance(duration, int) for duration in durations), "Error: durations must be a list of ints."
86 |
87 | remote_paths = self.loop.run_until_complete(self.__async_logging_common_routine(
88 | mode="SCHEDULED", timestamps=timestamps, durations=durations, variables=variables, logging_dir=logging_dir))
89 |
90 | async def _async_schedule_logging(variables, timestamps, durations, transfer, logging_dir):
91 | # checks types and if no variables are specified, stream all watcher variables (default)
92 | latest_timestamp = await self._async_get_latest_timestamp()
93 |
94 | local_paths = {}
95 | if transfer:
96 | self.connect_ssh() # start ssh connection
97 |
98 | async def _async_check_if_file_exists_and_start_copying(var, timestamp):
99 |
100 | diff_stamps = timestamp - latest_timestamp
101 | while True:
102 | _has_file_been_created = 0
103 |
104 | remote_file_size = self.sftp_client.stat(
105 | remote_paths[var]).st_size
106 |
107 | if remote_file_size > 0: # white till first buffers are written into the file
108 | _has_file_been_created = 1
109 | _print_info(
110 | f"Logging started for {var}...")
111 | break # Break the loop if the remote file size is non-zero
112 |
113 | if _has_file_been_created:
114 | break
115 |
116 | # Wait before checking again
117 | await asyncio.sleep(diff_stamps//(2*self.sample_rate))
118 |
119 | # when file has been created
120 | local_path = os.path.join(
121 | logging_dir, os.path.basename(remote_paths[var]))
122 |
123 | # if file already exists, throw a warning and add number at the end of the filename
124 | local_paths[var] = self._generate_local_filename(
125 | local_path)
126 |
127 | copying_task = self.__copy_file_in_chunks(
128 | remote_paths[var], local_paths[var])
129 | self._active_copying_tasks.append(copying_task)
130 |
131 | _active_checking_tasks = []
132 | for idx, var in enumerate(variables):
133 | check_task = self.loop.create_task(
134 | _async_check_if_file_exists_and_start_copying(var, timestamps[idx]))
135 | _active_checking_tasks.append(check_task)
136 |
137 | # wait for the longest duration
138 | await asyncio.sleep(max(durations)//(self.sample_rate))
139 | self._logging_mode = "OFF"
140 |
141 | # wait for all the files to be created
142 | await asyncio.gather(*_active_checking_tasks, return_exceptions=True)
143 | # wait for all the files to be copied
144 | await asyncio.gather(*self._active_copying_tasks, return_exceptions=True)
145 | self._active_copying_tasks.clear()
146 | _active_checking_tasks.clear()
147 | if self.sftp_client:
148 | self.disconnect_ssh()
149 |
150 | # async version (non blocking)
151 | # async def _async_cleanup():
152 | # await asyncio.gather(*self._active_copying_tasks, return_exceptions=True)
153 | # self._active_copying_tasks.clear()
154 | # self.sftp_client.close()
155 | # asyncio.run(_async_cleanup())
156 |
157 | return {"local_paths": local_paths, "remote_paths": remote_paths}
158 |
159 | return self.loop.run_until_complete(_async_schedule_logging(variables=variables, timestamps=timestamps, durations=durations, transfer=transfer, logging_dir=logging_dir))
160 |
161 | async def __async_logging_common_routine(self, mode, timestamps=[], durations=[], variables=[], logging_dir="./"):
162 | # checks types and if no variables are specified, stream all watcher variables (default)
163 | variables = self._var_arg_checker(variables)
164 |
165 | if not os.path.exists(logging_dir):
166 | os.makedirs(logging_dir)
167 |
168 | if self.is_logging():
169 | self.loop.create_task(self._async_stop_logging())
170 |
171 | # self.connect_ssh() # start ssh connection
172 |
173 | self._logging_mode = mode
174 |
175 | remote_files, remote_paths = {}, {}
176 |
177 | await self._async_send_ctrl_msg({"watcher": [
178 | {"cmd": "log", "timestamps": timestamps, "durations": durations, "watchers": variables}]})
179 |
180 | _list = await self._async_list()
181 | for idx, var in enumerate(variables):
182 | remote_files[var] = _list["watchers"][idx]["logFileName"]
183 | remote_paths[var] = f'/root/Bela/projects/{self.project_name}/{remote_files[var]}'
184 |
185 | _print_info(
186 | f"Started logging variables {variables}... Run stop_logging() to stop logging.")
187 |
188 | return remote_paths
189 |
190 | async def _async_stop_logging(self, variables=[]):
191 | """Stops logging session.
192 |
193 | Args:
194 | variables (list of str, optional): List of variables to stop logging. If none is passed, logging is stopped for all variables in the watcher. Defaults to [].
195 | """
196 | self._logging_mode = "OFF"
197 | if variables == []:
198 | # if no variables specified, stop streaming all watcher variables (default)
199 | variables = [var["name"] for var in self.watcher_vars]
200 |
201 | await self._async_send_ctrl_msg(
202 | {"watcher": [{"cmd": "unlog", "watchers": variables}]})
203 |
204 | _print_info(f"Stopped logging variables {variables}...")
205 |
206 | await asyncio.gather(*self._active_copying_tasks, return_exceptions=True)
207 | self._active_copying_tasks.clear()
208 | if self.sftp_client:
209 | self.disconnect_ssh()
210 |
211 | def stop_logging(self, variables=[]):
212 | """ Stops logging session. Sync wrapper for _async_stop_logging().
213 |
214 | Args:
215 | variables (list of str, optional): List of variables to stop logging. If none is passed, logging is stopped for all variables in the watcher. Defaults to [].
216 | """
217 |
218 | return self.loop.run_until_complete(self._async_stop_logging(variables))
219 |
220 | # -- binary file parsing method
221 |
222 | def read_binary_file(self, file_path, timestamp_mode):
223 | """ Reads a binary file generated by the logger and returns a dictionary with the file contents.
224 |
225 | Args:
226 | file_path (str): Path of the file to be read.
227 | timestamp_mode (str): Timestamp mode of the variable. Can be "dense" or "sparse".
228 |
229 | Returns:
230 | dict: Dictionary with the file contents.
231 | """
232 | if file_path is None:
233 | _print_error("Error: No file path provided.")
234 | return
235 | file_size = os.path.getsize(file_path)
236 | assert file_size != 0, f"Error: The size of {file_path} is 0."
237 |
238 | def _parse_null_terminated_string(file):
239 | result = ""
240 | while True:
241 | char = file.read(1).decode('utf-8')
242 | if char == '\0':
243 | break
244 | result += char
245 | return result
246 |
247 | with open(file_path, "rb") as file:
248 |
249 | # parse header
250 | name = _parse_null_terminated_string(file)
251 | var_name = _parse_null_terminated_string(file)
252 | _type = _parse_null_terminated_string(file)
253 | pid = struct.unpack("I", file.read(struct.calcsize("I")))[0]
254 | pid_id = struct.unpack("I", file.read(struct.calcsize("I")))[0]
255 |
256 | # if header size is not a multiple of 4, we need to add padding
257 | header_size = len(name) + len(var_name) + len(_type) + \
258 | 3 + struct.calcsize("I") + struct.calcsize("I")
259 | if header_size % 4 != 0:
260 | file.read(4 - header_size % 4) # padding
261 |
262 | _parse_type = 'i' if _type == 'j' else _type # struct does not understand 'j'
263 |
264 | # parse data buffers
265 | parsed_buffers = []
266 | while True:
267 | # Read file buffer by buffer
268 | try:
269 | data = file.read(self.get_buffer_size(
270 | _parse_type, timestamp_mode))
271 | if len(data) == 0:
272 | break # No more data to read
273 | _parsed_buffer = self._parse_binary_data(
274 | data, timestamp_mode, _parse_type)
275 | parsed_buffers.append(_parsed_buffer)
276 |
277 | except struct.error as e:
278 | _print_error(str(e))
279 | break # No more data to read
280 |
281 | return {
282 | "project_name": name,
283 | "var_name": var_name,
284 | "type": _type,
285 | # "pid": pid,
286 | # "pid_id": pid_id,
287 | "buffers": parsed_buffers
288 | }
289 |
290 | # -- file transfer utils --
291 | # expand copy_file_from_bela method in Watcher
292 |
293 | def __copy_file_in_chunks(self, remote_path, local_path, chunk_size=2**12):
294 | """ Copies a file from the remote path to the local path in chunks. This function is called by start_logging() if transfer=True.
295 |
296 | Args:
297 | remote_path (str): Path to the file in Bela.
298 | local_path (str): Path to the file in the local machine (where the file is copied to)
299 | chunk_size (int, optional): Chunk size. Defaults to 2**12.
300 |
301 | Returns:
302 | asyncio.Task: Task that copies the file in chunks.
303 | """
304 |
305 | async def async_copy_file_in_chunks(remote_path, local_path, chunk_size=2**12):
306 |
307 | while True:
308 | # Wait for a second before checking again
309 | await asyncio.sleep(1) # TODO can this be lower?
310 |
311 | try:
312 | remote_file = self.sftp_client.open(remote_path, 'rb')
313 | remote_file_size = self.sftp_client.stat(
314 | remote_path).st_size
315 |
316 | if remote_file_size > 0: # white till first buffers are written into the file
317 | break # Break the loop if the remote file size is non-zero
318 |
319 | except FileNotFoundError:
320 | _print_error(
321 | f"Remote file '{remote_path}' does not exist.")
322 | return None
323 |
324 | try:
325 | async with aiofiles.open(local_path, 'wb') as local_file:
326 | while True:
327 | chunk = remote_file.read(chunk_size)
328 | # keep checking file whilst logging is still going on (in case a variable fills the buffers slowly)
329 | if not chunk and self._logging_mode == "OFF":
330 | await asyncio.sleep(0.1) # flushed data
331 | break
332 | await local_file.write(chunk)
333 | _print_ok(
334 | f"\rTransferring {remote_path}-->{local_path}...", end="", flush=True)
335 | await asyncio.sleep(0.1)
336 | chunk = remote_file.read()
337 | if chunk:
338 | await local_file.write(chunk)
339 | remote_file.close()
340 | _print_ok("Done.")
341 |
342 | except Exception as e:
343 | _print_error(
344 | f"Error while transferring file: {e}.")
345 | return None
346 |
347 | finally:
348 | await self._async_remove_item_from_list(self._active_copying_tasks, asyncio.current_task())
349 |
350 | return self.loop.create_task(async_copy_file_in_chunks(remote_path, local_path, chunk_size))
351 |
352 | def copy_all_bin_files_in_project(self, dir="./", verbose=True):
353 | """ Copies all .bin files in the specified remote directory using SFTP.
354 |
355 | Args:
356 | dir (str, optional): Path to the local directory where the files are copied to. Defaults to "./".
357 | verbose (bool, optional): Show info messages. Defaults to True.
358 | """
359 | remote_path = f'/root/Bela/projects/{self.project_name}'
360 | try:
361 | self.connect_ssh()
362 | copy_tasks = self._action_on_all_bin_files_in_project(
363 | "copy", dir)
364 |
365 | # wait until all files are copied
366 | self.loop.run_until_complete(asyncio.gather(
367 | *copy_tasks, return_exceptions=True))
368 |
369 | if verbose:
370 | _print_ok(
371 | f"All .bin files in {remote_path} have been copied to {dir}.")
372 | except Exception as e:
373 | _print_error(
374 | f"Error copying .bin files in {remote_path}: {e}")
375 | finally:
376 | self.disconnect_ssh()
377 |
378 | def finish_copying_file(self, remote_path, local_path): # TODO test
379 | """Finish copying file if it was interrupted. This function is used to copy the remaining part of a file that was interrupted during the copy process.
380 |
381 | Args:
382 | remote_path (str): Path to the file in Bela.
383 | local_path (str): Path to the file in the local machine (where the file is copied to)
384 | """
385 | self.connect_ssh()
386 |
387 | try:
388 | remote_file = self.sftp_client.open(remote_path, 'rb')
389 | remote_file_size = self.sftp_client.stat(
390 | remote_path).st_size
391 | except FileNotFoundError:
392 | _print_error(
393 | f"Remote file '{remote_path}' does not exist.")
394 | self.disconnect_ssh()
395 | return None
396 | if not os.path.exists(local_path):
397 | _print_error(
398 | f"Local file '{local_path}' does not exist. If you want to copy a file that hasn't been partially copied yet, use copy_file_from_bela() instead.")
399 | self.disconnect_ssh()
400 | return None
401 | local_file_size = os.path.getsize(local_path)
402 |
403 | try:
404 | if local_file_size < remote_file_size:
405 | # Calculate the remaining part to copy
406 | remaining_size = remote_file_size - local_file_size
407 | # Use readv to read the remaining part of the file
408 | chunks = [(local_file_size, remaining_size)]
409 | data = remote_file.readv(chunks)
410 |
411 | _print_ok(
412 | f"\rTransferring {remote_path}-->{local_path}...", end="", flush=True)
413 | # Append the data to the local file
414 | with open(local_path, 'ab') as local_file:
415 | local_file.write(data)
416 | _print_ok("Done.")
417 | else:
418 | _print_error(
419 | "Local file is already up-to-date or larger than the remote file.")
420 | except Exception as e:
421 | _print_error(f"Error finishing file copy: {e}")
422 |
423 | self.disconnect_ssh()
424 |
425 | def delete_file_from_bela(self, remote_path, verbose=True):
426 | """Deletes a file from the remote path in Bela.
427 |
428 | Args:
429 | remote_path (str): Path to the remote file to be deleted.
430 | """
431 | self.connect_ssh()
432 | self.loop.run_until_complete(
433 | self._async_delete_file_from_bela(remote_path, verbose))
434 | self.disconnect_ssh()
435 |
436 | def delete_all_bin_files_in_project(self, verbose=True):
437 | """ Deletes all .bin files in the specified remote directory using SFTP.
438 | """
439 | remote_path = f'/root/Bela/projects/{self.project_name}'
440 | try:
441 | self.connect_ssh()
442 | deletion_tasks = self._action_on_all_bin_files_in_project(
443 | "delete")
444 |
445 | # wait until all files are deleted
446 | self.loop.run_until_complete(asyncio.gather(
447 | *deletion_tasks, return_exceptions=True))
448 |
449 | if verbose:
450 | _print_ok(
451 | f"All .bin files in {remote_path} have been removed.")
452 | except Exception as e:
453 | _print_error(
454 | f"Error deleting .bin files in {remote_path}: {e}")
455 | finally:
456 | self.disconnect_ssh()
457 |
458 | async def _async_delete_file_from_bela(self, remote_path, verbose=True):
459 | # this function doesn't return until the file has been deleted
460 | try:
461 | self.sftp_client.stat(remote_path) # check if file exists
462 | except FileNotFoundError:
463 | _print_error(
464 | f"Error: Remote file '{remote_path}' does not exist.")
465 | return
466 |
467 | while True:
468 | await asyncio.sleep(0.1) # Adjust the interval as needed
469 | try:
470 | # Attempt to remove the file
471 | self.sftp_client.remove(remote_path)
472 | except FileNotFoundError:
473 | # File does not exist, it has been successfully removed
474 | if verbose:
475 | _print_ok(
476 | f"File '{remote_path}' has been removed from Bela.")
477 | break
478 | except Exception as e:
479 | _print_error(
480 | f"Error while deleting file in Bela: {e} ")
481 | break
482 |
483 | def _action_on_all_bin_files_in_project(self, action, local_dir=None):
484 | # List all files in the remote directory
485 | remote_path = f'/root/Bela/projects/{self.project_name}'
486 | file_list = self.sftp_client.listdir(remote_path)
487 | if len(file_list) == 0:
488 | _print_warning(f"No .bin files in {remote_path}.")
489 | return
490 |
491 | # Iterate through the files and delete .bin files
492 | tasks = []
493 |
494 | for file_name in file_list:
495 | if file_name.endswith('.bin'):
496 | remote_file_path = f"{remote_path}/{file_name}"
497 | if action == "delete":
498 | task = self.loop.create_task(
499 | self._async_delete_file_from_bela(remote_file_path))
500 | elif action == "copy":
501 | local_filename = os.path.join(local_dir, file_name)
502 | task = self.loop.create_task(
503 | self._async_copy_file_from_bela(remote_file_path, local_filename))
504 | else:
505 | raise ValueError(f"Invalid action: {action}")
506 | tasks.append(task)
507 |
508 | return tasks
509 |
510 | def __del__(self):
511 | super().__del__()
512 | self.disconnect_ssh() # disconnect ssh
513 |
--------------------------------------------------------------------------------
/pybela/Monitor.py:
--------------------------------------------------------------------------------
1 | from .Streamer import Streamer
2 |
3 |
4 | class Monitor(Streamer):
5 | def __init__(self, ip="192.168.7.2", port=5555, data_add="gui_data", control_add="gui_control"):
6 | """ Monitor class
7 |
8 | Args:
9 | ip (str, optional): Remote address IP. If using internet over USB, the IP won't work, pass "bela.local". Defaults to "192.168.7.2".
10 | port (int, optional): Remote address port. Defaults to 5555.
11 | data_add (str, optional): Data endpoint. Defaults to "gui_data".
12 | control_add (str, optional): Control endpoint. Defaults to "gui_control".
13 | """
14 |
15 | super(Monitor, self).__init__(ip, port, data_add, control_add)
16 |
17 | self._mode = "MONITOR"
18 |
19 | def connect(self):
20 | if (super().connect()):
21 | # longer queue for monitor since each buffer has only one value
22 | self.streaming_buffers_queue_length = 2000
23 |
24 | @property
25 | def values(self):
26 | """ Get monitored values from last monitoring session
27 |
28 | Returns:
29 | dict of dicts of list: Dict containing the monitored buffers for each variable in the watcher
30 | """
31 | values = {}
32 | for var in self.streaming_buffers_queue:
33 | values[var] = {"timestamps": [], "values": []}
34 | for _buffer in self.streaming_buffers_queue[var]:
35 | values[var]["timestamps"].append(_buffer["timestamp"])
36 | values[var]["values"].append(_buffer["value"])
37 | return values
38 |
39 | def peek(self, variables=[]):
40 | """ Peek at variables
41 |
42 | Args:
43 | variables (list, optional): List of variables to peek at. If no variables are specified, stream all watcher variables (default).
44 |
45 | Returns:
46 | dict: Dictionary of variables with their values
47 | """
48 |
49 | async def _async_peek(variables):
50 | # checks types and if no variables are specified, stream all watcher variables (default)
51 | variables = self._var_arg_checker(variables)
52 | self._peek_response = {var: None for var in variables}
53 | self.start_monitoring(variables, [1]*len(variables))
54 | await self._peek_response_available.wait()
55 | self._peek_response_available.clear()
56 | peeked_values = self._peek_response
57 | # set _peek_response again to None so that peek is not notified every time a new buffer is received (see Streamer._process_data_msg)
58 | self._peek_response = None
59 | await self._async_stop_monitoring(variables)
60 |
61 | return peeked_values
62 |
63 | return self.loop.run_until_complete(_async_peek(variables))
64 |
65 | # using list
66 | # res = self.list()
67 | # return {var: next(r["value"] for r in res if r["name"] == var) for var in variables}
68 |
69 | def start_monitoring(self, variables=[], periods=[], saving_enabled=False, saving_filename="monitor.txt", saving_dir="./"):
70 | """
71 | Starts the monitoring session. The session can be stopped with stop_monitoring().
72 |
73 | If no variables are specified, all watcher variables are monitored. If saving_enabled is True, the monitored data is saved to a local file. If saving_filename is None, the default filename is used with the variable name appended to its start. The filename is automatically incremented if it already exists.
74 |
75 | Args:
76 | variables (list, optional): List of variables to be streamed. Defaults to [].
77 | periods (list, optional): List of monitoring periods. Defaults to [].
78 | saving_enabled (bool, optional): Enables/disables saving monitored data to local file. Defaults to False.
79 | saving_filename (str, optional) Filename for saving the monitored data. Defaults to None.
80 | saving_dir (str, optional): Directory for saving the monitored data. Defaults to "/.".
81 | """
82 |
83 | variables = self._var_arg_checker(variables)
84 | self._periods = self._check_periods(periods, variables)
85 |
86 | self.start_streaming(
87 | variables=variables, periods=self._periods, saving_enabled=saving_enabled, saving_filename=saving_filename, saving_dir=saving_dir)
88 |
89 | def monitor_n_values(self, variables=[], periods=[], n_values=1000, saving_enabled=False, saving_filename="monitor.txt"):
90 | """
91 | Monitors a given number of values. Since the data comes in buffers of a predefined size, always an extra number of frames will be monitored (unless the number of frames is a multiple of the buffer size).
92 |
93 | Note: This function will block the main thread until n_values have been monitored. Since the monitored values come in blocks, the actual number of returned frames monitored may be higher than n_values, unless n_values is a multiple of the block size (monitor._streaming_block_size).
94 |
95 | To avoid blocking, use the async version of this function:
96 | monitor_task = asyncio.create_task(monitor.async_monitor_n_values(variables, n_values, periods, saving_enabled, saving_filename))
97 | and retrieve the monitored buffer using:
98 | monitored_buffers_queue = await monitor_task
99 |
100 | Args:
101 | variables (list, optional): List of variables to be monitored. Defaults to [].
102 | periods (list, optional): List of monitoring periods. Defaults to [].
103 | n_values (int, optional): Number of values to monitor for each variable. Defaults to 1000.
104 | delay (int, optional): _description_. Defaults to 0.
105 | saving_enabled (bool, optional): Enables/disables saving monitored data to local file. Defaults to False.
106 | saving_filename (_type_, optional) Filename for saving the monitored data. Defaults to None.
107 |
108 | Returns:
109 | monitored_buffers_queue (dict): Dict containing the monitored buffers for each streamed variable.
110 | """
111 | variables = self._var_arg_checker(variables)
112 | self._periods = self._check_periods(periods, variables)
113 | self.stream_n_values(variables, self._periods, n_values,
114 | saving_enabled, saving_filename)
115 | return self.values
116 |
117 | async def async_monitor_n_values(self, variables=[], periods=[], n_values=1000, saving_enabled=False, saving_filename=None):
118 | """
119 | Asynchronous version of monitor_n_values(). Usage:
120 | monitor_task = asyncio.create_task(monitor.async_monitor_n_values(variables, periods, n_values, saving_enabled, saving_filename))
121 | and retrieve the monitored buffer using:
122 | monitoring_buffers_queue = await monitor_task
123 |
124 |
125 | Args:
126 | variables (list, optional): List of variables to be monitored. Defaults to [].
127 | periods (list, optional): List of monitoring period. Defaults to [].
128 | n_values (int, optional): Number of values to monitor for each variable. Defaults to 1000.
129 | saving_enabled (bool, optional): Enables/disables saving monitored data to local file. Defaults to False.
130 | saving_filename (_type_, optional) Filename for saving the monitored data. Defaults to None.
131 |
132 | Returns:
133 | deque: Monitored buffers queue
134 | """
135 | variables = self._var_arg_checker(variables)
136 | self._periods = self._check_periods(periods, variables)
137 |
138 | self.async_stream_n_values(variables, self._periods, n_values,
139 | saving_enabled, saving_filename)
140 |
141 | async def _async_stop_monitoring(self, variables=[]):
142 | await self._async_stop_streaming(variables)
143 | self._monitored_vars = None
144 | self._periods = None
145 | return {var: self.values[var] for var in self.values if self.values[var]["timestamps"] != []}
146 |
147 | def stop_monitoring(self, variables=[]):
148 | """
149 | Stops the current monitoring session for the given variables. If no variables are passed, the monitoring of all variables is interrupted.
150 |
151 | Args:
152 | variables (list, optional): List of variables to stop monitoring. Defaults to [].
153 |
154 | Returns:
155 | dict of dicts of lists: Dict containing the monitored buffers for each monitored variable
156 | """
157 | self.stop_streaming(variables)
158 | self._monitored_vars = None # reset monitored variables
159 | # return only nonempty variables
160 | return {var: self.values[var] for var in self.values if self.values[var]["timestamps"] != []}
161 |
162 | def load_data_from_file(self, filename, flatten=True):
163 | """
164 | Loads data from a file saved through the saving_enabled function in start_monitoring() or monitor_n_values(). The file should contain a list of dicts, each dict containing a variable name and a list of values. The list of dicts should be separated by newlines.
165 | Args:
166 | filename (str): Filename
167 | flatten (bool, optional): If True, the returned list of values is flattened. Defaults to True.
168 |
169 | Returns:
170 | dict of lists: Dict with "timestamps" and "values" list if flatten is True, otherwise a list of dicts with "timestamp" and "value" keys.
171 | """
172 | loaded_buffers = super().load_data_from_file(filename)
173 | if flatten:
174 | flatten_loaded_buffers = {"timestamps": [], "values": []}
175 | for _buffer in loaded_buffers:
176 | flatten_loaded_buffers["timestamps"].append(
177 | _buffer["timestamp"])
178 | flatten_loaded_buffers["values"].append(_buffer["value"])
179 | return flatten_loaded_buffers
180 | else:
181 | return loaded_buffers
182 |
--------------------------------------------------------------------------------
/pybela/__init__.py:
--------------------------------------------------------------------------------
1 | from .Watcher import Watcher
2 | from .Streamer import Streamer
3 | from .Logger import Logger
4 | from .Monitor import Monitor
5 | from .Controller import Controller
6 |
7 | __all__ = ['Watcher', 'Streamer', 'Logger', 'Monitor', 'Controller']
8 |
--------------------------------------------------------------------------------
/pybela/utils.py:
--------------------------------------------------------------------------------
1 | class _bcolors:
2 | HEADER = '\033[95m'
3 | OKBLUE = '\033[94m'
4 | OKCYAN = '\033[96m'
5 | OKGREEN = '\033[92m'
6 | WARNING = '\033[93m'
7 | FAIL = '\033[91m'
8 | ENDC = '\033[0m'
9 | BOLD = '\033[1m'
10 | UNDERLINE = '\033[4m'
11 |
12 |
13 | def _print_error(message):
14 | print(_bcolors.FAIL + message + _bcolors.ENDC)
15 |
16 |
17 | def _print_info(message):
18 | print(_bcolors.OKBLUE + message + _bcolors.ENDC)
19 |
20 |
21 | def _print_warning(message):
22 | print(_bcolors.WARNING + message + _bcolors.ENDC)
23 |
24 |
25 | def _print_ok(message, end='\n', flush=False):
26 | print(_bcolors.OKGREEN + message + _bcolors.ENDC, end=end, flush=flush)
27 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "pybela"
3 | version = "2.0.3"
4 | authors = [
5 | { name="Teresa Pelinski", email="teresapelinski@gmail.com" }
6 | ]
7 | description = "pybela allows interfacing with the Bela embedded audio platform using python. It offers a convenient way to stream data between Bela and python, in both directions. In addition to data streaming, pybela supports data logging, as well as variable monitoring and control functionalities."
8 | readme = "readme.md"
9 | requires-python = ">=3.9"
10 | dependencies = [
11 | "aiofiles==24.1.0",
12 | "bitarray==3.0.0",
13 | "bokeh==2.4.3",
14 | "ipykernel==6.29.5",
15 | "jupyter==1.1.1",
16 | "jupyter-bokeh==3.0.5",
17 | "matplotlib>=3.9.4",
18 | "nest-asyncio==1.6.0",
19 | "notebook==7.2.2",
20 | "numpy==1.26.0",
21 | "pandas==2.2.3",
22 | "panel==0.14.4",
23 | "paramiko==3.5.0",
24 | "websockets==14.1",
25 | ]
26 | license = "LGPL-3.0"
27 | classifiers=[
28 | "Programming Language :: Python :: 3",
29 | "Operating System :: OS Independent"
30 | ]
31 | keywords = ["Bela", "physical computing", "data", "audio", "python", "embedded systems", "real-time", "monitoring", "control", "streaming", "sensor"]
32 |
33 | [project.urls]
34 | Homepage = "https://github.com/BelaPlatform/pybela"
35 | Documentation = "https://belaplatform.github.io/pybela/"
36 |
37 |
38 | [project.optional-dependencies]
39 | dev = [
40 | "build>=1.2.2.post1",
41 | "pip-chill>=1.0.3",
42 | "pipdeptree>=2.26.0",
43 | "scipy>=1.13.1",
44 | "sphinx==7.4.7",
45 | "sphinx-rtd-theme==3.0.2",
46 | "toml>=0.10.2",
47 | "twine>=6.1.0",
48 | "uvloop>=0.21.0",
49 | ]
50 |
51 | [project.scripts]
52 | test = "test:run_tests"
53 | test-send = "test:run_test_send"
54 |
55 | [build-system]
56 | requires = ["setuptools", "wheel", "build"]
57 | build-backend = "setuptools.build_meta"
58 |
59 | [tool.setuptools.packages.find]
60 | include = ["pybela*"]
61 | exclude = ["dev", "watcher", "tutorials"]
62 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # pybela
2 |
3 | pybela allows interfacing with [Bela](https://bela.io/), the embedded audio platform, using python. It offers a convenient way to stream data between Bela and python, in both directions. In addition to data streaming, pybela supports data logging, as well as variable monitoring and control functionalities.
4 |
5 | Below, you can find instructions to install pybela. You can find code examples at `tutorials/` and `test/`. The docs are available at [https://belaplatform.github.io/pybela/](https://belaplatform.github.io/pybela/).
6 |
7 | pybela was developed with a machine learning use-case in mind. For a complete pipeline including data acquisition, processing, model training, and deployment (including rapid cross-compilation) check the [pybela-pytorch-xc-tutorial](https://github.com/pelinski/pybela-pytorch-xc-tutorial). You can also check out the [deep-learning-for-bela](https://github.com/pelinski/deep-learning-for-bela) resource list.
8 |
9 | ## Installation and set up
10 |
11 | You will need to (1) install the python package in your laptop, (2) set the Bela branch to `dev` and (3) add the watcher library to your Bela project.
12 |
13 | ### 1. Installing the python package
14 |
15 | You can install this library using `pip`:
16 |
17 | ```python
18 | pip install pybela
19 | ```
20 |
21 | ### 2. Set the Bela branch to `dev`
22 |
23 | `pybela` is relies on the `watcher` library, which currently only works with the Bela `dev` branch. To set your Bela to the `dev` branch, you can follow the instructions below.
24 |
25 | **Note:** if you just flashed the Bela image, the date and time on the Bela board might be wrong, and the Bela libraries might not build correctly after changing the Bela branch. To set the correct date, you can either run (in the host)
26 |
27 | ```bash
28 | ssh root@bela.local "date -s \"`date '+%Y%m%d %T %z'`\""
29 | ```
30 |
31 | or just open the IDE in your browser (type `bela.local` in the address bar).
32 |
33 | #### Option A: Bela connected to internet
34 |
35 | If your Bela is connected to internet, you can ssh into your Bela (`ssh root@bela.local`) and change the branch:
36 |
37 | ```bash
38 | # in Bela
39 | cd Bela
40 | git checkout dev
41 | make -f Makefile.libraries cleanall && make coreclean
42 | ```
43 |
44 | #### Option B: Bela not connected to internet
45 |
46 | If your Bela is not connected to internet, you can change the branch by cloning the Bela repository into your laptop and then pushing the `dev` branch to your Bela.
47 | To do that, first clone the Bela repository into your laptop:
48 |
49 | ```bash
50 | # in laptop
51 | git clone --recurse-submodules https://github.com/belaPlatform/bela
52 | cd Bela
53 | ```
54 |
55 | Then add your Bela as a remote and push the `dev` branch to your Bela:
56 |
57 | ```bash
58 | # in laptop
59 | git remote add board root@bela.local:Bela/
60 | git checkout dev
61 | git push -f board dev:tmp
62 | ```
63 |
64 | Then ssh into your Bela (`ssh root@bela.local`) and change the branch:
65 |
66 | ```bash
67 | # in Bela
68 | cd Bela
69 | git checkout tmp
70 | make -f Makefile.libraries cleanall && make coreclean
71 | ```
72 |
73 | You can check the commit hash by running `git rev-parse --short HEAD` either on Bela or your laptop.
74 |
75 | ### 3. Add the watcher library to your project
76 |
77 | For pybela to be able to communicate with your Bela device, you will need to add the watcher library to your Bela project. To do so, you will need to add the files `Watcher.h` and `Watcher.cpp` to your Bela project. You can do this by copying the files from the `watcher` repository into your Bela project.
78 |
79 | First you need to clone this repository, **don't forget to add the `--recurse-submodules` flag to the `git` command** to populate the `watcher/` folder:
80 |
81 | ```bash
82 | # in laptop
83 | git clone --recurse-submodules https://github.com/BelaPlatform/pybela.git
84 | ```
85 |
86 | Then you can copy the files to your Bela project:
87 |
88 | ```bash
89 | # in laptop
90 | cd pybela/
91 | scp watcher/Watcher.h watcher/Watcher.cpp root@bela.local:Bela/projects/your-project/
92 | ```
93 |
94 | ## Getting started
95 |
96 | ### Modes of operation
97 |
98 | pybela has three different modes of operation:
99 |
100 | - **Streaming**: continuously send data from Bela to python (**NEW: and from python to Bela!** check the [tutorial](tutorials/notebooks/3_Streamer-python-to-Bela.ipynb)).
101 | - **Logging**: log data in a file in Bela and then retrieve it in python.
102 | - **Monitoring**: monitor the value of variables in the Bela code from python.
103 | - **Controlling**: control the value of variables in the Bela code from python.
104 |
105 | You can check the **tutorials** at `tutorials/`for more detailed information and usage of each of the modes. You can also check`test/test.py` for a quick overview of the library.
106 |
107 | ### Running the tutorials
108 |
109 | The quickest way to get started is to start a jupyter notebook server and run the tutorials. If you haven't done it yet, install the python package as explained in the Installation section. If you don't have the `jupyter notebook` package installed, you can install it by running:
110 |
111 | ```bash
112 | pip install notebook
113 | ```
114 |
115 | Once installed, start a jupyter notebook server by running:
116 |
117 | ```bash
118 | jupyter notebook
119 | ```
120 |
121 | This should open a window in your browser from which you can look for the `tutorials/notebooks` folder and open the examples.
122 |
123 | ### Basic usage
124 |
125 | pybela allows you to access variables defined in your Bela code from python. To do so, you need to define the variables you want to access in your Bela code using the `Watcher` library.
126 |
127 | #### Bela side
128 |
129 | For example, if you want to access the variable `myvar` from python, you need to declare the variable in your Bela with the Watcher template:
130 |
131 | ```cpp
132 | #include
133 | Watcher myvar("myvar");
134 | ```
135 |
136 | You will also need to add the following lines to your `setup` loop:
137 |
138 | ```cpp
139 | bool setup(BelaContext *context, void *userData)
140 | {
141 | Bela_getDefaultWatcherManager()->getGui().setup(context->projectName);
142 | Bela_getDefaultWatcherManager()->setup(context->audioSampleRate);
143 | // your code here...
144 | }
145 | ```
146 |
147 | You will also need to add the following lines to your render loop:
148 |
149 | ```cpp
150 | void render(BelaContext *context, void *userData)
151 | {
152 | for(unsigned int n = 0; n < context->audioFrames; n++) {
153 | uint64_t frames = context->audioFramesElapsed + n;
154 | Bela_getDefaultWatcherManager()->tick(frames);
155 | // your code here...
156 | }
157 | }
158 | ```
159 |
160 | you can see an example [here](./test/bela-test/render.cpp).
161 |
162 | #### Python side
163 |
164 | Once the variable is declared with the Watcher template, you can stream, log, monitor and control its value from python. For example, to stream the value of `myvar` from python, you can do:
165 |
166 | ```python
167 | from pybela import Streamer
168 | streamer = Streamer()
169 | streamer.connect()
170 | streamer.start_streaming("myvar")
171 | ```
172 |
173 | to terminate the streaming, you can run:
174 |
175 | ```python
176 | streamer.stop_streaming()
177 | ```
178 |
179 | ## Example projects
180 |
181 | - [pybela-drumsynth](https://github.com/jorshi/pybela-drumsynth): Audio-driven drum synthesis. This project takes audio from a microphone to control a drum synthesiser using onset detection and audio feature extraction. It uses pybela to capture an audio dataset and runs a torch model on Bela.
182 | - [faab-hyperparams](https://github.com/pelinski/faab-hyperparams/): Project that explores sonification of latent spaces of a Transformer Autoencoder model. This project uses pybela to capture training data, and to stream data to the laptop which runs a pytorch model. The output of the model can be sent back to Bela in real-time or sent through OSC to another device.
183 |
184 | ## Testing
185 |
186 | _This library has been tested with Bela at `dev` branch commit `69cdf75a` and watcher at `main` commit `903573a`._
187 |
188 | To run pybela's tests first copy the `bela-test` code into your Bela, compile and run it:
189 |
190 | ```bash
191 | rsync -rvL test/bela-test root@bela.local:Bela/projects/
192 | ssh root@bela.local 'make -C /root/Bela run PROJECT=bela-test'
193 | ```
194 |
195 | Create the python environment and activate it. Our preferred environment is `uv` but you can use your environment manager of choice and install the dependencies in `requirements.txt`.
196 |
197 | ```bash
198 | uv venv
199 | source .venv/bin/activate
200 | ```
201 |
202 | you can run the python tests by running:
203 |
204 | ```bash
205 | uv run python test/test.py
206 | ```
207 |
208 | ## Building
209 |
210 | You can build pybela using `uv`:
211 |
212 | ```bash
213 | uv build
214 | ```
215 |
216 | To test the build, connect your Bela to the computer. The following script will test the packaged build by running the `twine` tests, creating a new temporal virtual environment, installing the library from the dist files, and running the pybela test routine. This will take a few minutes.
217 |
218 | ```bash
219 | sh dev/test-dist.sh
220 | ```
221 |
222 | You can also test the docs with:
223 |
224 | ```bash
225 | sh dev/test-docs.sh
226 | ```
227 |
228 | ## To do and known issues
229 |
230 | - [ ] **Fix**: logger with automatic transfer too slow for large datasets
231 | - [ ] **Issue:** Monitor and streamer/controller can't be used simultaneously – This is due to both monitor and streamer both using the same websocket connection and message format. This could be fixed by having a different message format for the monitor and the streamer (e.g., adding a header to the message)
232 | - [ ] **Issue:** The plotting routine does not work when variables are updated at different rates.
233 | - [ ] **Issue**: The plotting routine does not work for the monitor (it only works for the streamer)
234 | - [ ] **Code refactor:** There are two routines for generating filenames (for Streamer and for Logger). This should be unified.
235 | - [ ] **Possible feature:** Flexible backend buffer size for streaming: if the assign rate of variables is too slow, the buffers might not be filled and hence not sent (since the data flushed is not collected in the frontend), and there will be long delays between the variable assign and the data being sent to the frontend.
236 | - [ ] **Issue:** Flushed buffers are not collected after `stop_streaming` in the frontend.
237 |
238 | ## License
239 |
240 | This library is distributed under LGPL, the GNU Lesser General Public License (LGPL 3.0), available [here](https://www.gnu.org/licenses/lgpl-3.0.en.html).
241 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | aiofiles==24.1.0
2 | bitarray==3.0.0
3 | bokeh==2.4.3
4 | ipykernel==6.29.5
5 | jupyter==1.1.1
6 | jupyter-bokeh==3.0.5
7 | nest-asyncio==1.6.0
8 | notebook==7.2.2
9 | numpy==1.26.0
10 | pandas==2.2.3
11 | panel==0.14.4
12 | paramiko==3.5.0
13 | websockets==14.1
--------------------------------------------------------------------------------
/test/__init__.py:
--------------------------------------------------------------------------------
1 | from .test import run_tests
2 | from .test_send import run_test_send
3 |
4 | __all__ = [ "run_tests", "run_test_send"]
--------------------------------------------------------------------------------
/test/bela-test-send/Watcher.cpp:
--------------------------------------------------------------------------------
1 | ../../watcher/Watcher.cpp
--------------------------------------------------------------------------------
/test/bela-test-send/Watcher.h:
--------------------------------------------------------------------------------
1 | ../../watcher/Watcher.h
--------------------------------------------------------------------------------
/test/bela-test-send/render.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 |
5 | Watcher myvar1("myvar1");
6 | Watcher myvar2("myvar2");
7 |
8 | std::vector*> myVars = {&myvar1, &myvar2};
9 |
10 | struct ReceivedBuffer {
11 | uint32_t bufferId;
12 | char bufferType[4];
13 | uint32_t bufferLen;
14 | uint32_t empty;
15 | std::vector bufferData;
16 | };
17 | ReceivedBuffer receivedBuffer;
18 | uint receivedBufferHeaderSize;
19 | uint64_t totalReceivedCount;
20 |
21 |
22 | bool binaryDataCallback(const std::string& addr, const WSServerDetails* id, const unsigned char* data, size_t size, void* arg) {
23 |
24 | totalReceivedCount++;
25 |
26 | std::memcpy(&receivedBuffer, data, receivedBufferHeaderSize);
27 | receivedBuffer.bufferData.resize(receivedBuffer.bufferLen);
28 | std::memcpy(receivedBuffer.bufferData.data(), data + receivedBufferHeaderSize, receivedBuffer.bufferLen * sizeof(float)); // data is a pointer to the beginning of the data
29 |
30 | printf("\ntotal received count: %llu, total data size: %zu, bufferId: %d, bufferType: %s, bufferLen: %d \n", totalReceivedCount, size, receivedBuffer.bufferId, receivedBuffer.bufferType,
31 | receivedBuffer.bufferLen);
32 |
33 | Bela_getDefaultWatcherManager()->tick(totalReceivedCount);
34 | int _id = receivedBuffer.bufferId;
35 | if (_id >= 0 && _id < myVars.size()) {
36 |
37 | for (size_t i = 0; i < receivedBuffer.bufferData.size(); ++i) {
38 | *myVars[_id] = receivedBuffer.bufferData[i];
39 | }
40 | }
41 |
42 | return true;
43 | }
44 |
45 | bool setup(BelaContext* context, void* userData) {
46 |
47 | Bela_getDefaultWatcherManager()->getGui().setup(context->projectName);
48 | Bela_getDefaultWatcherManager()->setup(context->audioSampleRate); // set sample rate in watcher
49 |
50 | for (int i = 0; i < 2; ++i) {
51 | Bela_getDefaultWatcherManager()->getGui().setBuffer('f', 1024);
52 | }
53 |
54 | Bela_getDefaultWatcherManager()->getGui().setBinaryDataCallback(binaryDataCallback);
55 |
56 | receivedBufferHeaderSize = sizeof(receivedBuffer.bufferId) + sizeof(receivedBuffer.bufferType) + sizeof(receivedBuffer.bufferLen) + sizeof(receivedBuffer.empty);
57 | totalReceivedCount = 0;
58 | Bela_getDefaultWatcherManager()->tick(totalReceivedCount); // init the watcher
59 |
60 | return true;
61 | }
62 |
63 | void render(BelaContext* context, void* userData) {
64 | // DataBuffer& receivedBuffer =
65 | // Bela_getDefaultWatcherManager()->getGui().getDataBuffer(dataBufferId);
66 | // float* data = receivedBuffer.getAsFloat();
67 | }
68 |
69 | void cleanup(BelaContext* context, void* userData) {
70 | }
71 |
--------------------------------------------------------------------------------
/test/bela-test/Watcher.cpp:
--------------------------------------------------------------------------------
1 | ../../watcher/Watcher.cpp
--------------------------------------------------------------------------------
/test/bela-test/Watcher.h:
--------------------------------------------------------------------------------
1 | ../../watcher/Watcher.h
--------------------------------------------------------------------------------
/test/bela-test/render.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | Watcher myvar("myvar");
3 | Watcher myvar2("myvar2");
4 | Watcher myvar3("myvar3", WatcherManager::kTimestampSample);
5 | Watcher myvar4("myvar4", WatcherManager::kTimestampSample);
6 | Watcher myvar5("myvar5");
7 |
8 |
9 |
10 | #include
11 | #include
12 |
13 | bool setup(BelaContext *context, void *userData)
14 | {
15 | Bela_getDefaultWatcherManager()->getGui().setup(context->projectName);
16 | Bela_getDefaultWatcherManager()->setup(context->audioSampleRate); // set sample rate in watcher
17 |
18 | return true;
19 | }
20 |
21 | void render(BelaContext *context, void *userData)
22 | {
23 |
24 | static size_t count = 0;
25 | if(1) // if(count++ >= context->audioSampleRate * 0.6 / context->audioFrames)
26 | {
27 | //rt_printf("%.5f %.5f\n\r", float(myvar), float(myvar2));
28 | static int pastC = -1;
29 | static int pastAC = -1;
30 | int c = Bela_getDefaultWatcherManager()->getGui().numConnections();
31 | int ac = Bela_getDefaultWatcherManager()->getGui().numActiveConnections();
32 | if(c != pastC || ac != pastAC)
33 | rt_printf("connected %d %d\n", c, ac);
34 | pastC = c;
35 | pastAC = ac;
36 | count = 0;
37 | }
38 |
39 | for(unsigned int n = 0; n < context->audioFrames; n++) {
40 | uint64_t frames = context->audioFramesElapsed + n;
41 | Bela_getDefaultWatcherManager()->tick(frames);
42 |
43 | myvar = frames;
44 | myvar2 = frames; // log a dense variable densely: good
45 | myvar5 = frames;
46 |
47 | if(frames % 12 == 0){ // log a sparse variable sparsely: good
48 | myvar3 = frames;
49 | myvar4 = frames;
50 | }
51 |
52 | }
53 | }
54 |
55 | void cleanup(BelaContext *context, void *userData)
56 | {
57 | }
58 |
--------------------------------------------------------------------------------
/test/bela-test/sketch.js:
--------------------------------------------------------------------------------
1 | ../../watcher/sketch.js
--------------------------------------------------------------------------------
/test/readme.md:
--------------------------------------------------------------------------------
1 | # testing
2 |
3 | pybela has been tested with [Bela](https://github.com/BelaPlatform/Bela) at `dev` branch commit `d5f0d6f` and [watcher](https://github.com/BelaPlatform/watcher) at `main` commit `903573a`.
4 |
5 | The watcher code is already included in `bela-test`. You can update your Bela API code following [these instructions](readme.md).
6 |
7 | To run the tests, copy the `bela-test` code into your Bela, add the `Watcher`` library compile and run it:
8 |
9 | ```bash
10 | rsync -rvL test/bela-test test/bela-test-send root@bela.local:Bela/projects/
11 | ssh root@bela.local "make -C Bela stop Bela PROJECT=bela-test run"
12 | ```
13 |
14 | Once the `bela-test` project is running on Bela, you can run the python tests by running:
15 |
16 | ```bash
17 | uv run python test.py
18 | ```
19 |
20 | You can also test the `bela-test-send` project by running:
21 |
22 | ```bash
23 | ssh root@bela.local "make -C Bela stop Bela PROJECT=bela-test run"
24 | ```
25 |
26 | and then running the python tests with:
27 |
28 | ```bash
29 | uv run test-send.py
30 | ```
31 |
--------------------------------------------------------------------------------
/test/test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import os
3 | import numpy as np
4 | from pybela import Watcher, Streamer, Logger, Monitor, Controller
5 |
6 | # os.environ["PYTHONASYNCIODEBUG"] = "1"
7 |
8 | # all tests should be run with Bela connected and the bela-test project (in test/bela-test) running on the board
9 |
10 |
11 | class test_Watcher(unittest.TestCase):
12 |
13 | def setUp(self):
14 | self.watcher = Watcher()
15 | self.watcher.connect()
16 |
17 | def tearDown(self):
18 | self.watcher.cleanup()
19 | # pass
20 |
21 | def test_list(self):
22 | self.assertEqual(len(self.watcher.list()["watchers"]), len(self.watcher.watcher_vars),
23 | "Length of list should be equal to number of watcher variables")
24 |
25 | def test_start_stop(self):
26 | self.watcher.disconnect()
27 | self.assertTrue(self.watcher.ws_ctrl.close,
28 | "Watcher ctrl websocket should be closed after stop")
29 | self.assertTrue(self.watcher.ws_data.close,
30 | "Watcher data websocket should be closed after stop")
31 |
32 |
33 | class test_Streamer(unittest.TestCase):
34 |
35 | def setUp(self):
36 | self.streamer = Streamer()
37 | self.streamer.connect()
38 | self.streaming_vars = [
39 | "myvar", # dense double
40 | "myvar2", # dense uint
41 | "myvar3", # sparse uint
42 | "myvar4" # sparse double
43 | ]
44 | self.saving_dir = "./test"
45 | self.saving_filename = "test_streamer_save.txt"
46 |
47 | def tearDown(self):
48 | self.streamer.cleanup()
49 |
50 | def test_stream_n_values(self):
51 | n_values = 40
52 |
53 | streaming_buffer = self.streamer.stream_n_values(
54 | variables=self.streaming_vars[:2], n_values=n_values)
55 |
56 | # calc number of buffers needed to get n_values
57 | buffer_sizes = [
58 | self.streamer.get_data_length(var["type"], var["timestamp_mode"])
59 | for var in self.streamer.watcher_vars if var["name"] in self.streaming_vars[:2]]
60 | n_buffers = -(-n_values // min(buffer_sizes))
61 |
62 | # test data
63 | self.assertTrue(all(len(self.streamer.streaming_buffers_data[
64 | var]) >= n_values for var in self.streaming_vars[:2]), "The streamed flat buffers for every variable should have at least n_values")
65 | self.assertTrue(all(len(streaming_buffer[
66 | var]) == n_buffers for var in self.streaming_vars[:2]), "The streaming buffers queue should have at least n_values/buffer_size buffers for every variable")
67 |
68 | def __test_buffers(self, mode):
69 | # test data
70 | for var in [v for v in self.streamer.watcher_vars if v["name"] in self.streaming_vars]:
71 |
72 | # check buffers in streaming_buffers_queue have the right length
73 | self.assertTrue(all(len(_buffer["data"]) == var["data_length"] for _buffer in self.streamer.streaming_buffers_queue[var["name"]]),
74 | f"The data buffers in self.streamer.streaming_buffers_queue should have a length of {var['data_length']} for a variable of type {var['type']} ")
75 | loaded = self.streamer.load_data_from_file(
76 | os.path.join(self.saving_dir, f"{var['name']}_{self.saving_filename}"))
77 | # check that the loader buffers have the right length
78 | self.assertTrue(all(len(_buffer["data"]) == var["data_length"] for _buffer in loaded),
79 | "The loaded data buffers should have a length of {var['data_length']} for a variable of type {var['type']} ")
80 | # check that the number of buffers saved is the same as the number of buffers in self.streamer.streaming_buffers_queue
81 | self.assertTrue(len(self.streamer.streaming_buffers_queue[var['name']]) == len(loaded),
82 | "The number of buffers saved should be equal to the number of buffers in self.streamer.streaming_buffers_queue (considering the queue length is long enough)")
83 |
84 | # check continuity of frames (only works for dense variables)
85 | if mode != "schedule":
86 | for var in [v for v in self.streamer.watcher_vars if v["name"] in self.streaming_vars[:2]]:
87 | for _buffer in self.streamer.streaming_buffers_queue[var["name"]]:
88 | self.assertEqual(_buffer["ref_timestamp"], _buffer["data"][0],
89 | "The ref_timestamp and the first item of data buffer should be the same")
90 | self.assertEqual(_buffer["ref_timestamp"]+var["data_length"]-1, _buffer["data"][-1],
91 | "The last data item should be equal to the ref_timestamp plus the length of the buffer") # this test will fail if the Bela program has been streaming for too long and there are truncating errors. If this test fails, try stopping and rerunning hte Bela program again
92 | # delete files
93 | for var in self.streaming_vars:
94 | remove_file(os.path.join(self.saving_dir,
95 | f"{var}_{self.saving_filename}"))
96 |
97 | def test_start_stop_streaming(self):
98 | self.streamer.streaming_buffers_queue_length = 1000
99 |
100 | # delete any existing test files
101 | for var in self.streaming_vars:
102 | remove_file(os.path.join(self.saving_dir,
103 | f"{var}_{self.saving_filename}"))
104 |
105 | # stream with saving
106 | self.streamer.start_streaming(variables=self.streaming_vars,
107 | saving_enabled=True, saving_filename=self.saving_filename, saving_dir=self.saving_dir)
108 | # check streaming mode is FOREVER after start_streaming is called
109 | self.assertEqual(self.streamer._streaming_mode, "FOREVER",
110 | "Streaming mode should be FOREVER after start_streaming")
111 | # wait for some data to be streamed
112 | self.streamer.wait(0.5)
113 | self.streamer.stop_streaming(variables=self.streaming_vars)
114 | # check streaming mode is OFF after stop_streaming
115 |
116 | self.assertEqual(self.streamer._streaming_mode, "OFF",
117 | "Streaming mode should be OFF after stop_streaming")
118 | self.__test_buffers(mode="start_stop")
119 |
120 | def test_scheduling_streaming(self):
121 | self.streamer.streaming_buffers_queue_length = 1000
122 | latest_timestamp = self.streamer.get_latest_timestamp()
123 | sample_rate = self.streamer.sample_rate
124 | timestamps = [latest_timestamp +
125 | sample_rate] * len(self.streaming_vars) # start streaming after ~1s
126 | durations = [sample_rate] * \
127 | len(self.streaming_vars) # stream for 1s
128 |
129 | self.streamer.schedule_streaming(variables=self.streaming_vars,
130 | timestamps=timestamps,
131 | durations=durations,
132 | saving_enabled=True,
133 | saving_dir=self.saving_dir,
134 | saving_filename=self.saving_filename)
135 |
136 | self.__test_buffers(mode="schedule")
137 |
138 | def test_on_buffer_callback(self):
139 | variables = ["myvar", "myvar5"] # dense double
140 |
141 | # test only on vars of the same type
142 |
143 | timestamps = {var: [] for var in variables}
144 | buffers = {var: [] for var in variables}
145 |
146 | def callback(buffer):
147 | timestamps[buffer["name"]].append(
148 | buffer["buffer"]["ref_timestamp"])
149 | buffers[buffer["name"]].append(buffer["buffer"]["data"])
150 |
151 | self.streamer.start_streaming(
152 | variables, saving_enabled=False, on_buffer_callback=callback)
153 |
154 | self.streamer.wait(0.5)
155 |
156 | self.streamer.stop_streaming(variables)
157 |
158 | for var in variables:
159 | for i in range(1, len(timestamps[var])):
160 | self.assertEqual(timestamps[var][i] - timestamps[var][i-1], 512,
161 | "The timestamps should be continuous. The callback is missing some buffer")
162 |
163 | def test_on_block_callback(self):
164 | variables = ["myvar", "myvar5"] # dense double
165 |
166 | timestamps = {var: [] for var in variables}
167 | buffers = {var: [] for var in variables}
168 |
169 | def callback(block):
170 | for buffer in block:
171 | var = buffer["name"]
172 | timestamps[var].append(buffer["buffer"]["ref_timestamp"])
173 | buffers[var].append(buffer["buffer"]["data"])
174 |
175 | self.streamer.start_streaming(
176 | variables, saving_enabled=False, on_block_callback=callback)
177 |
178 | self.streamer.wait(0.5)
179 |
180 | self.streamer.stop_streaming(variables)
181 |
182 | self.assertGreater(len(
183 | timestamps["myvar"]), 0, "The on_block_callback should have been called at least once")
184 |
185 | for var in variables:
186 | for i in range(1, len(timestamps[var])):
187 | self.assertEqual(timestamps[var][i] - timestamps[var][i-1], 512,
188 | "The timestamps should be continuous. The callback is missing some buffer")
189 |
190 |
191 | class test_Logger(unittest.TestCase):
192 |
193 | def setUp(self):
194 | self.logger = Logger()
195 | self.logger.connect()
196 |
197 | self.logging_vars = [
198 | "myvar", # dense double
199 | "myvar2", # dense uint
200 | "myvar3", # sparse uint
201 | "myvar4" # sparse double
202 | ]
203 | self.logging_dir = "./test"
204 |
205 | def tearDown(self):
206 | self.logger.cleanup()
207 |
208 | def _test_logged_data(self, logger, logging_vars, local_paths):
209 | # common routine to test the data in the logged files
210 | data = {}
211 | for var in logging_vars:
212 | data[var] = logger.read_binary_file(
213 | file_path=local_paths[var], timestamp_mode=logger.get_prop_of_var(var, "timestamp_mode"))
214 |
215 | # test data
216 | timestamp_mode = logger.get_prop_of_var(var, "timestamp_mode")
217 | for _buffer in data[var]["buffers"]:
218 | self.assertEqual(_buffer["ref_timestamp"], _buffer["data"][0],
219 | "The ref_timestamp and the first item of data buffer should be the same")
220 | self.assertEqual(logger.get_prop_of_var(var, "data_length"), len(_buffer["data"]),
221 | "The length of the buffer should be equal to the data_length property of the variable")
222 | if _buffer["data"][-1] == 0: # buffer has padding at the end
223 | continue
224 | if timestamp_mode == "dense":
225 | self.assertEqual(_buffer["ref_timestamp"]+logger.get_prop_of_var(var, "data_length")-1, _buffer["data"][-1],
226 | f"{var} {local_paths[var]} The last data item should be equal to the ref_timestamp plus the length of the buffer")
227 | elif timestamp_mode == "sparse":
228 | inferred_timestamps = [_ + _buffer["ref_timestamp"]
229 | for _ in _buffer["rel_timestamps"]]
230 | self.assertEqual(
231 | inferred_timestamps, _buffer["data"], "The timestamps should be equal to the ref_timestamp plus the relative timestamps (sparse logging)")
232 |
233 | def test_logged_files_with_transfer(self):
234 | # log with transfer
235 | file_paths = self.logger.start_logging(
236 | variables=self.logging_vars, transfer=True, logging_dir=self.logging_dir)
237 | self.logger.wait(0.5)
238 | self.logger.stop_logging()
239 |
240 | # test logged data
241 | self._test_logged_data(self.logger, self.logging_vars,
242 | file_paths["local_paths"])
243 |
244 | # clean local log files
245 | for var in file_paths["local_paths"]:
246 | remove_file(file_paths["local_paths"][var])
247 | # clean all remote log files in project
248 | self.logger.delete_all_bin_files_in_project()
249 |
250 | def test_logged_files_wo_transfer(self):
251 |
252 | # logging without transfer
253 | file_paths = self.logger.start_logging(
254 | variables=self.logging_vars, transfer=False, logging_dir=self.logging_dir)
255 | self.logger.wait(0.5)
256 | self.logger.stop_logging()
257 |
258 | # transfer files from bela
259 | local_paths = {}
260 | for var in file_paths["remote_paths"]:
261 | filename = os.path.basename(file_paths["remote_paths"][var])
262 | local_paths[var] = self.logger.copy_file_from_bela(remote_path=file_paths["remote_paths"][var],
263 | local_path=filename)
264 |
265 | # test logged data
266 | self._test_logged_data(self.logger, self.logging_vars, local_paths)
267 |
268 | # clean log files
269 | for var in self.logging_vars:
270 | remove_file(local_paths[var])
271 | # self.logger.delete_file_from_bela(
272 | # file_paths["remote_paths"][var])
273 | self.logger.delete_all_bin_files_in_project()
274 |
275 | def test_scheduling_logging(self):
276 | latest_timestamp = self.logger.get_latest_timestamp()
277 | sample_rate = self.logger.sample_rate
278 | timestamps = [latest_timestamp +
279 | sample_rate] * len(self.logging_vars) # start logging after ~1s
280 | durations = [sample_rate] * len(self.logging_vars) # log for 1s
281 |
282 | file_paths = self.logger.schedule_logging(variables=self.logging_vars,
283 | timestamps=timestamps,
284 | durations=durations,
285 | transfer=True,
286 | logging_dir=self.logging_dir)
287 |
288 | self._test_logged_data(self.logger, self.logging_vars,
289 | file_paths["local_paths"])
290 |
291 | # clean local log files
292 | for var in file_paths["local_paths"]:
293 | if os.path.exists(file_paths["local_paths"][var]):
294 | os.remove(file_paths["local_paths"][var])
295 | self.logger.delete_all_bin_files_in_project()
296 |
297 | # # clean all remote log files in project
298 | # for var in file_paths["remote_paths"]:
299 | # self.logger.delete_file_from_bela(
300 | # file_paths["remote_paths"][var])
301 |
302 |
303 | class test_Monitor(unittest.TestCase):
304 | def setUp(self):
305 | self.monitor_vars = ["myvar", "myvar2", "myvar3", "myvar4"]
306 | self.period = 1000
307 | self.saving_filename = "test_monitor_save.txt"
308 | self.saving_dir = "./test"
309 |
310 | self.monitor = Monitor()
311 | self.monitor.connect()
312 |
313 | def tearDown(self):
314 | self.monitor.cleanup()
315 |
316 | def test_peek(self):
317 | peeked_values = self.monitor.peek() # peeks at all variables by default
318 | for var in peeked_values:
319 | self.assertEqual(peeked_values[var]["timestamp"], peeked_values[var]["value"],
320 | "The timestamp of the peeked variable should be equal to the value")
321 |
322 | def test_period_monitor(self):
323 | self.monitor.start_monitoring(
324 | variables=self.monitor_vars[:2],
325 | periods=[self.period]*len(self.monitor_vars[:2]))
326 | self.monitor.wait(0.5)
327 | monitored_values = self.monitor.stop_monitoring()
328 |
329 | for var in self.monitor_vars[:2]: # assigned at every frame n
330 | self.assertTrue(np.all(np.diff(monitored_values[var]["timestamps"]) == self.period),
331 | "The timestamps of the monitored variables should be spaced by the period")
332 | if var in ["myvar", "myvar2"]: # assigned at each frame n
333 | self.assertTrue(np.all(np.diff(monitored_values[var]["values"]) == self.period),
334 | "The values of the monitored variables should be spaced by the period")
335 |
336 | def test_monitor_n_values(self):
337 | n_values = 25
338 | monitored_buffer = self.monitor.monitor_n_values(
339 | variables=self.monitor_vars[:2],
340 | periods=[self.period]*len(self.monitor_vars[:2]), n_values=n_values)
341 |
342 | for var in self.monitor_vars[:2]:
343 | self.assertTrue(np.all(np.diff(self.monitor.values[var]["timestamps"]) == self.period),
344 | "The timestamps of the monitored variables should be spaced by the period")
345 | self.assertTrue(np.all(np.diff(self.monitor.values[var]["values"]) == self.period),
346 | "The values of the monitored variables should be spaced by the period")
347 | self.assertTrue(all(len(self.monitor.streaming_buffers_data[
348 | var]) >= n_values for var in self.monitor_vars[:2]), "The streamed flat buffers for every variable should have at least n_values")
349 | self.assertTrue(all(len(monitored_buffer[
350 | var]["values"]) == n_values for var in self.monitor_vars[:2]), "The streaming buffers queue should have n_value for every variable")
351 |
352 | def test_save_monitor(self):
353 |
354 | # delete any existing test files
355 | for var in self.monitor_vars:
356 | if os.path.exists(f"{var}_{self.saving_filename}"):
357 | os.remove(f"{var}_{self.saving_filename}")
358 |
359 | self.monitor.start_monitoring(
360 | variables=self.monitor_vars,
361 | periods=[self.period]*len(self.monitor_vars),
362 | saving_enabled=True,
363 | saving_filename=self.saving_filename,
364 | saving_dir=self.saving_dir)
365 | self.monitor.wait(0.5)
366 | monitored_buffers = self.monitor.stop_monitoring()
367 |
368 | for var in self.monitor_vars:
369 | loaded_buffers = self.monitor.load_data_from_file(os.path.join(self.saving_dir,
370 | f"{var}_{self.saving_filename}"))
371 |
372 | self.assertEqual(loaded_buffers["timestamps"], monitored_buffers[var]["timestamps"],
373 | "The timestamps of the loaded buffer should be equal to the timestamps of the monitored buffer")
374 | self.assertEqual(loaded_buffers["values"], monitored_buffers[var]["values"],
375 | "The values of the loaded buffer should be equal to the values of the monitored buffer")
376 |
377 | for var in self.monitor_vars:
378 | remove_file(os.path.join(self.saving_dir,
379 | f"{var}_{self.saving_filename}"))
380 |
381 |
382 | class test_Controller(unittest.TestCase):
383 | def setUp(self):
384 | self.controlled_vars = ["myvar", "myvar2", "myvar3", "myvar4"]
385 |
386 | self.controller = Controller()
387 | self.controller.connect()
388 |
389 | def tearDown(self):
390 | self.controller.cleanup()
391 |
392 | def test_start_stop_controlling(self):
393 | self.controller.start_controlling(variables=self.controlled_vars)
394 |
395 | self.assertEqual(self.controller.get_controlled_status(variables=self.controlled_vars), {
396 | var: True for var in self.controlled_vars}, "The controlled status of the variables should be True after start_controlling")
397 |
398 | self.controller.stop_controlling(variables=self.controlled_vars)
399 |
400 | self.assertEqual(self.controller.get_controlled_status(variables=self.controlled_vars), {
401 | var: False for var in self.controlled_vars}, "The controlled status of the variables should be False after stop_controlling")
402 |
403 | def test_send_value(self):
404 | # TODO add streamer to check values are being sent
405 | self.controller.start_controlling(variables=self.controlled_vars)
406 |
407 | set_value = 4.6
408 |
409 | self.controller.send_value(
410 | variables=self.controlled_vars, values=[set_value]*len(self.controlled_vars))
411 | self.controller.wait(0.1) # wait for the values to be set
412 |
413 | _controlled_values = self.controller.get_value(
414 | variables=self.controlled_vars) # avoid multiple calls to list
415 |
416 | integer_types = ["i", "j"]
417 | expected_values = [int(set_value) if self.controller.get_prop_of_var(
418 | var, "type") in integer_types else set_value for var in self.controlled_vars]
419 |
420 | for idx, var in enumerate(self.controlled_vars):
421 | self.assertTrue(
422 | _controlled_values[var] == expected_values[idx], "The controlled value should be 4")
423 |
424 |
425 | def remove_file(file_path):
426 | if os.path.exists(file_path):
427 | os.remove(file_path)
428 |
429 |
430 | def run_tests():
431 | # run all tests
432 | # unittest.main(verbosity=2)
433 |
434 | # select which tests to run
435 | n = 2
436 | for i in range(n):
437 |
438 | print(f"\n\n....Running test {i+1}/{n}")
439 |
440 | suite = unittest.TestSuite()
441 | suite.addTests([
442 | # watcher
443 | test_Watcher('test_list'),
444 | test_Watcher('test_start_stop'),
445 | # streamer
446 | test_Streamer('test_stream_n_values'),
447 | test_Streamer('test_start_stop_streaming'),
448 | test_Streamer('test_scheduling_streaming'),
449 | test_Streamer('test_on_buffer_callback'),
450 | test_Streamer('test_on_block_callback'),
451 | # logger
452 | test_Logger('test_logged_files_with_transfer'),
453 | test_Logger('test_logged_files_wo_transfer'),
454 | test_Logger('test_scheduling_logging'),
455 | # monitor
456 | test_Monitor('test_peek'),
457 | test_Monitor('test_period_monitor'),
458 | test_Monitor('test_monitor_n_values'),
459 | test_Monitor('test_save_monitor'),
460 | # controller
461 | test_Controller('test_start_stop_controlling'),
462 | test_Controller('test_send_value')
463 | ])
464 | # suite.addTest(test_Streamer('test_on_block_callback'))
465 | runner = unittest.TextTestRunner(verbosity=2)
466 | runner.run(suite)
467 |
468 | if __name__ == '__main__':
469 | run_tests()
--------------------------------------------------------------------------------
/test/test_send.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from pybela import Streamer
3 | import numpy as np
4 |
5 | streamer = Streamer()
6 | variables = ["myvar1", "myvar2"]
7 |
8 |
9 | # can't be merged with test.py because in the render.cpp the watcher needs to be 'ticked' when iterating the buffer, not at every audio frame!
10 |
11 | # TOOD test other types (int, double, uint, char)
12 |
13 |
14 | class test_Sender(unittest.TestCase):
15 | def test_send_buffer(self):
16 | if streamer.connect():
17 |
18 | streamer.start_streaming(variables)
19 |
20 | # Pack the data into binary format
21 | # >I means big-endian unsigned int, 4s means 4-byte string, pad with x for empty bytes
22 |
23 | for id in [0, 1]:
24 | # buffers are only sent from Bela to the host once full, so it needs to be 1024 long to be sent
25 | buffer_id, buffer_type, buffer_length = id, 'f', 1024
26 | data_list = np.arange(1, buffer_length+1, 1)
27 | streamer.send_buffer(buffer_id, buffer_type,
28 | buffer_length, data_list)
29 |
30 | streamer.wait(0.1) # wait for the buffer to be sent
31 |
32 | for var in variables:
33 | assert np.array_equal(
34 | streamer.streaming_buffers_data[var], data_list), "Data sent and received are not the same"
35 |
36 | streamer.stop_streaming()
37 |
38 |
39 | def run_test_send():
40 | suite = unittest.TestSuite()
41 | suite.addTest(test_Sender('test_send_buffer'))
42 | runner = unittest.TextTestRunner(verbosity=2)
43 | runner.run(suite)
44 |
45 | if __name__ == '__main__':
46 | run_test_send()
--------------------------------------------------------------------------------
/tutorials/bela-code/bela2python2bela/Watcher.cpp:
--------------------------------------------------------------------------------
1 | ../../../watcher/Watcher.cpp
--------------------------------------------------------------------------------
/tutorials/bela-code/bela2python2bela/Watcher.h:
--------------------------------------------------------------------------------
1 | ../../../watcher/Watcher.h
--------------------------------------------------------------------------------
/tutorials/bela-code/bela2python2bela/render.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 | #include
6 |
7 | #define NUM_OUTPUTS 2
8 | #define MAX_EXPECTED_BUFFER_SIZE 1024
9 |
10 | Watcher pot1("pot1");
11 | Watcher pot2("pot2");
12 |
13 | uint gPot1Ch = 0;
14 | uint gPot2Ch = 1;
15 |
16 | std::vector> circularBuffers(NUM_OUTPUTS);
17 |
18 | size_t circularBufferSize = 30 * 1024;
19 | size_t prefillSize = 2.5 * 1024;
20 | uint32_t circularBufferWriteIndex[NUM_OUTPUTS] = {0};
21 | uint32_t circularBufferReadIndex[NUM_OUTPUTS] = {0};
22 |
23 | struct ReceivedBuffer {
24 | uint32_t bufferId;
25 | char bufferType[4];
26 | uint32_t bufferLen;
27 | uint32_t empty;
28 | std::vector bufferData;
29 | };
30 | ReceivedBuffer receivedBuffer;
31 | uint receivedBufferHeaderSize;
32 | uint64_t totalReceivedCount; // total number of received buffers
33 |
34 | unsigned int gAudioFramesPerAnalogFrame;
35 | float gInvAudioFramesPerAnalogFrame;
36 | float gInverseSampleRate;
37 | float gPhase1;
38 | float gPhase2;
39 | float gFrequency1 = 440.0f;
40 | float gFrequency2 = 880.0f;
41 |
42 | // this callback is called every time a buffer is received from python. it parses the received data into the ReceivedBuffer struct, and then writes the data to the circular buffer which is read in the
43 | // render function
44 | bool binaryDataCallback(const std::string& addr, const WSServerDetails* id, const unsigned char* data, size_t size, void* arg) {
45 |
46 | if (totalReceivedCount == 0) {
47 | RtThread::setThisThreadPriority(1);
48 | }
49 |
50 | totalReceivedCount++;
51 |
52 | // parse buffer header
53 | std::memcpy(&receivedBuffer, data, receivedBufferHeaderSize);
54 | receivedBuffer.bufferData.resize(receivedBuffer.bufferLen);
55 | // parse buffer data
56 | std::memcpy(receivedBuffer.bufferData.data(), data + receivedBufferHeaderSize, receivedBuffer.bufferLen * sizeof(float));
57 |
58 | // write the data onto the circular buffer
59 | int _id = receivedBuffer.bufferId;
60 | if (_id >= 0 && _id < NUM_OUTPUTS) {
61 | for (size_t i = 0; i < receivedBuffer.bufferLen; ++i) {
62 | circularBuffers[_id][circularBufferWriteIndex[_id]] = receivedBuffer.bufferData[i];
63 | circularBufferWriteIndex[_id] = (circularBufferWriteIndex[_id] + 1) % circularBufferSize;
64 | }
65 | }
66 |
67 | return true;
68 | }
69 |
70 | bool setup(BelaContext* context, void* userData) {
71 |
72 | Bela_getDefaultWatcherManager()->getGui().setup(context->projectName);
73 | Bela_getDefaultWatcherManager()->setup(context->audioSampleRate); // set sample rate in watcher
74 |
75 | gAudioFramesPerAnalogFrame = context->audioFrames / context->analogFrames;
76 | gInvAudioFramesPerAnalogFrame = 1.0 / gAudioFramesPerAnalogFrame;
77 | gInverseSampleRate = 1.0 / context->audioSampleRate;
78 |
79 | // initialize the Gui buffers and circular buffers
80 | for (int i = 0; i < NUM_OUTPUTS; ++i) {
81 | Bela_getDefaultWatcherManager()->getGui().setBuffer('f', MAX_EXPECTED_BUFFER_SIZE);
82 | circularBuffers[i].resize(circularBufferSize, 0.0f);
83 | // the write index is given some "advantage" (prefillSize) so that the read pointer does not catch up the write pointer
84 | circularBufferWriteIndex[i] = prefillSize % circularBufferSize;
85 | }
86 |
87 | Bela_getDefaultWatcherManager()->getGui().setBinaryDataCallback(binaryDataCallback);
88 |
89 | // vars and preparation for parsing the received buffer
90 | receivedBufferHeaderSize = sizeof(receivedBuffer.bufferId) + sizeof(receivedBuffer.bufferType) + sizeof(receivedBuffer.bufferLen) + sizeof(receivedBuffer.empty);
91 | totalReceivedCount = 0;
92 | receivedBuffer.bufferData.reserve(MAX_EXPECTED_BUFFER_SIZE);
93 |
94 | return true;
95 | }
96 |
97 | void render(BelaContext* context, void* userData) {
98 | for (unsigned int n = 0; n < context->audioFrames; n++) {
99 | uint64_t frames = context->audioFramesElapsed + n;
100 |
101 | if (gAudioFramesPerAnalogFrame && !(n % gAudioFramesPerAnalogFrame)) {
102 | Bela_getDefaultWatcherManager()->tick(frames * gInvAudioFramesPerAnalogFrame); // watcher timestamps
103 |
104 | // read sensor values and put them in the watcher
105 | pot1 = analogRead(context, n / gAudioFramesPerAnalogFrame, gPot1Ch);
106 | pot2 = analogRead(context, n / gAudioFramesPerAnalogFrame, gPot2Ch);
107 |
108 | // read the values sent from python (they're in the circular buffer)
109 | for (unsigned int i = 0; i < NUM_OUTPUTS; i++) {
110 |
111 | if (totalReceivedCount > 0 && (circularBufferReadIndex[i] + 1) % circularBufferSize != circularBufferWriteIndex[i]) {
112 | circularBufferReadIndex[i] = (circularBufferReadIndex[i] + 1) % circularBufferSize;
113 | } else if (totalReceivedCount > 0) {
114 | rt_printf("The read pointer has caught the write pointer up in buffer %d – try increasing prefillSize\n", i);
115 | }
116 | }
117 | }
118 | float amp1 = circularBuffers[0][circularBufferReadIndex[0]];
119 | float amp2 = circularBuffers[1][circularBufferReadIndex[1]];
120 |
121 | float out = amp1 * sinf(gPhase1) + amp2 * sinf(gPhase2);
122 |
123 | for (unsigned int channel = 0; channel < context->audioOutChannels; channel++) {
124 | audioWrite(context, n, channel, out);
125 | }
126 |
127 | gPhase1 += 2.0f * (float)M_PI * gFrequency1 * gInverseSampleRate;
128 | if (gPhase1 > M_PI)
129 | gPhase1 -= 2.0f * (float)M_PI;
130 | gPhase2 += 2.0f * (float)M_PI * gFrequency2 * gInverseSampleRate;
131 | if (gPhase2 > M_PI)
132 | gPhase2 -= 2.0f * (float)M_PI;
133 | }
134 | }
135 |
136 | void cleanup(BelaContext* context, void* userData) {
137 | }
--------------------------------------------------------------------------------
/tutorials/bela-code/potentiometers/Watcher.cpp:
--------------------------------------------------------------------------------
1 | ../../../watcher/Watcher.cpp
--------------------------------------------------------------------------------
/tutorials/bela-code/potentiometers/Watcher.h:
--------------------------------------------------------------------------------
1 | ../../../watcher/Watcher.h
--------------------------------------------------------------------------------
/tutorials/bela-code/potentiometers/render.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | Watcher pot1("pot1");
3 | Watcher pot2("pot2");
4 |
5 | #include
6 | #include
7 |
8 | float gInverseSampleRate;
9 | int gAudioFramesPerAnalogFrame = 0;
10 |
11 | // Analog inputs for each potentiometer
12 | uint gPot1Ch = 0;
13 | uint gPot2Ch = 1;
14 |
15 | bool setup(BelaContext *context, void *userData)
16 | {
17 | Bela_getDefaultWatcherManager()->getGui().setup(context->projectName);
18 | Bela_getDefaultWatcherManager()->setup(context->audioSampleRate); // set sample rate in watcher
19 | gAudioFramesPerAnalogFrame = context->audioFrames / context->analogFrames;
20 | gInverseSampleRate = 1.0 / context->audioSampleRate;
21 | return true;
22 | }
23 |
24 | void render(BelaContext *context, void *userData)
25 | {
26 | for(unsigned int n = 0; n < context->audioFrames; n++) {
27 | if(gAudioFramesPerAnalogFrame && !(n % gAudioFramesPerAnalogFrame)) {
28 |
29 | uint64_t frames = context->audioFramesElapsed/gAudioFramesPerAnalogFrame + n/gAudioFramesPerAnalogFrame;
30 | Bela_getDefaultWatcherManager()->tick(frames); // watcher timestamps
31 |
32 | pot1 = analogRead(context, n/gAudioFramesPerAnalogFrame, gPot1Ch);
33 | pot2 = analogRead(context, n/gAudioFramesPerAnalogFrame, gPot2Ch);
34 |
35 | }
36 | }
37 | }
38 |
39 | void cleanup(BelaContext *context, void *userData)
40 | {
41 | }
42 |
--------------------------------------------------------------------------------
/tutorials/bela-code/timestamping/Watcher.cpp:
--------------------------------------------------------------------------------
1 | ../../../watcher/Watcher.cpp
--------------------------------------------------------------------------------
/tutorials/bela-code/timestamping/Watcher.h:
--------------------------------------------------------------------------------
1 | ../../../watcher/Watcher.h
--------------------------------------------------------------------------------
/tutorials/bela-code/timestamping/render.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | Watcher pot1("pot1");
3 | Watcher pot2("pot2", WatcherManager::kTimestampSample);
4 |
5 |
6 | #include
7 | #include
8 |
9 | float gInverseSampleRate;
10 | int gAudioFramesPerAnalogFrame = 0;
11 |
12 | // Analog inputs for each potentiometer
13 | uint gPot1Ch = 0;
14 | uint gPot2Ch = 1;
15 |
16 | bool setup(BelaContext *context, void *userData)
17 | {
18 | Bela_getDefaultWatcherManager()->getGui().setup(context->projectName);
19 | Bela_getDefaultWatcherManager()->setup(context->audioSampleRate); // set sample rate in watcher
20 | gAudioFramesPerAnalogFrame = context->audioFrames / context->analogFrames;
21 | gInverseSampleRate = 1.0 / context->audioSampleRate;
22 | return true;
23 | }
24 |
25 | void render(BelaContext *context, void *userData)
26 | {
27 | for(unsigned int n = 0; n < context->audioFrames; n++) {
28 | if(gAudioFramesPerAnalogFrame && !(n % gAudioFramesPerAnalogFrame)) {
29 |
30 | uint64_t frames = context->audioFramesElapsed/gAudioFramesPerAnalogFrame + n/gAudioFramesPerAnalogFrame;
31 | Bela_getDefaultWatcherManager()->tick(frames); // watcher timestamps
32 |
33 | pot1 = analogRead(context, n/gAudioFramesPerAnalogFrame, gPot1Ch);
34 |
35 | if (frames % 12==0){
36 | pot2 = analogRead(context, n/gAudioFramesPerAnalogFrame, gPot2Ch);
37 | }
38 | }
39 | }
40 | }
41 |
42 | void cleanup(BelaContext *context, void *userData)
43 | {
44 | }
45 |
--------------------------------------------------------------------------------
/tutorials/notebooks/1_Streamer-Bela-to-python-basics.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# pybela Tutorial 1: Streamer – Bela to python basics\n",
8 | "This notebook is a tutorial for the Streamer class in the pybela python library. You can use the Streamer to stream data from Bela to python or vice versa. \n",
9 | "\n",
10 | "In this tutorial we will be looking at sending data from Bela to python. The Streamer allows you to start and stop streaming, to stream a given number of data points, to plot the data as it arrives, and to save and load the streamed data into `.txt` files. \n",
11 | "\n",
12 | "The complete documentation for the pybela library can be found in [https://belaplatform.github.io/pybela/](https://belaplatform.github.io/pybela/).\n",
13 | "\n",
14 | "To run this tutorial, first copy the `bela-code/potentiometers` project onto Bela. If your Bela is connected to your laptop, you can run the cell below:"
15 | ]
16 | },
17 | {
18 | "cell_type": "code",
19 | "execution_count": null,
20 | "metadata": {},
21 | "outputs": [],
22 | "source": [
23 | "!rsync -rvL ../bela-code/potentiometers root@bela.local:Bela/projects"
24 | ]
25 | },
26 | {
27 | "cell_type": "markdown",
28 | "metadata": {},
29 | "source": [
30 | "Then you can compile and run the project using either the IDE or by running the following command in the Terminal:\n",
31 | "```bash\n",
32 | "ssh root@bela.local \"make -C Bela stop Bela PROJECT=potentiometers run\" \n",
33 | "```\n",
34 | "(Running this on a jupyter notebook will block the cell until the program is stopped on Bela.)"
35 | ]
36 | },
37 | {
38 | "cell_type": "markdown",
39 | "metadata": {},
40 | "source": [
41 | "### Setting up the circuit\n",
42 | "In this example we will be using two potentiometers as our analog signals, but you can connect whichever sensors you like to analog channels 0 and 1.\n",
43 | "\n",
44 | "Potentiometers have 3 pins. To connect a potentiometer to Bela, attach the left pin to the Bela 3.3V pin, the central pin to the desired analog input (e.g. 0) and the right pin to the Bela GND pin:\n",
45 | "\n",
46 | "\n",
47 | "
\n",
48 | "
\n",
49 | "\n",
50 | "### Taking a look at the Bela C++ code\n",
51 | "If you take a look into the Bela code (in `bela-code/potentiometers/render.cpp`), you will see that the variables `pot1` and `pot2` are defined in a particular way:\n",
52 | "\n",
53 | "```cpp\n",
54 | "Watcher pot1(\"pot1\");\n",
55 | "Watcher pot2(\"pot2\");\n",
56 | "```\n",
57 | "\n",
58 | "This means that the variables `pot1` and `pot2` are being \"watched\" and hence we can request their values to be streamed to this notebook using the pybela Streamer class. The watcher will stream a buffer containing timestamp and variable value information. Take a look at the `render` loop:\n",
59 | "\n",
60 | "```cpp\n",
61 | "void render(BelaContext *context, void *userData)\n",
62 | "{\n",
63 | "\tfor(unsigned int n = 0; n < context->audioFrames; n++) {\n",
64 | "\t\tif(gAudioFramesPerAnalogFrame && !(n % gAudioFramesPerAnalogFrame)) {\n",
65 | "\t\t\t\n",
66 | "\t\t\tuint64_t frames = context->audioFramesElapsed/gAudioFramesPerAnalogFrame + n/gAudioFramesPerAnalogFrame;\n",
67 | "\t\t\tBela_getDefaultWatcherManager()->tick(frames); // watcher timestamps\n",
68 | "\t\t\t\n",
69 | "\t\t\tpot1 = analogRead(context, n/gAudioFramesPerAnalogFrame, gPot1Ch);\n",
70 | "\t\t\tpot2 = analogRead(context, n/gAudioFramesPerAnalogFrame, gPot2Ch);\n",
71 | "\t\t\t\n",
72 | "\t\t}\n",
73 | "\t}\n",
74 | "}\n",
75 | "```\n",
76 | "\n",
77 | "we are reading the values of the potentiometer (with `analogRead()`) at every audio frame, and assigning them to their corresponding variable (`pot1` and `pot2`). In order for the Bela Watcher to know at which timestamp this happens, we need to \"tick\" the Watcher clock, we do this in line 30 with:\n",
78 | "```cpp\n",
79 | "\t\t\tBela_getDefaultWatcherManager()->tick(frames); // watcher timestamps\n",
80 | "```\n",
81 | "\n",
82 | "If you want to take a look at more advanced ways of watching variables, take a look at the Logger notebook. But enough with C++, let's take a look at the pybela Streamer class and its usage. \n",
83 | "\n",
84 | "### Getting started\n",
85 | "Once you have the circuit set up, build and run the Bela project `potentiometers`. Once running, we are ready to interact with it form this notebook. We'll start by importing some necessary libraries and setting the `BOKEH_ALLOW_WS_ORIGIN` environment that will allow us to visualise the bokeh plots (comment/uncomment depending on if you are running this notebook from a jupyter notebook or VSCode)."
86 | ]
87 | },
88 | {
89 | "cell_type": "code",
90 | "execution_count": null,
91 | "metadata": {},
92 | "outputs": [],
93 | "source": [
94 | "import pandas as pd\n",
95 | "from pybela import Streamer\n",
96 | "import os\n",
97 | "os.environ['BOKEH_ALLOW_WS_ORIGIN'] = \"1t4j54lsdj67h02ol8hionopt4k7b7ngd9483l5q5pagr3j2droq\" # uncomment if running on vscode\n",
98 | "# os.environ['BOKEH_ALLOW_WS_ORIGIN'] = \"localhost:8888\" # uncomment if running on jupyter"
99 | ]
100 | },
101 | {
102 | "cell_type": "markdown",
103 | "metadata": {},
104 | "source": [
105 | "Now let's initialise the streamer and connect it to the Bela websocket. If the connection fails, make sure Bela is connected to your laptop and that the `potentiometer` project is running on Bela."
106 | ]
107 | },
108 | {
109 | "cell_type": "code",
110 | "execution_count": null,
111 | "metadata": {},
112 | "outputs": [],
113 | "source": [
114 | "streamer = Streamer()\n",
115 | "streamer.connect()"
116 | ]
117 | },
118 | {
119 | "cell_type": "markdown",
120 | "metadata": {},
121 | "source": [
122 | "Let's start by streaming the values of potentiometer 1 and 2. For that, we call `streamer.start_streaming(variables=[\"pot1\", \"pot2\"])`. This will request the values of the variables `pot1` and `pot`. We can visualise those values as they arrive by plotting them using `streamer.plot_data(x_var=\"pot1\", y_vars=[\"pot1\", \"pot2\"], y_range=[0,1])`. The argument `x_var` determines which variable will provide the timestamps for the x axis, and the argument `y_vars` expects a list of variables that are currently being streamed or monitored. `y_range` determines the range of the y-axis."
123 | ]
124 | },
125 | {
126 | "cell_type": "code",
127 | "execution_count": null,
128 | "metadata": {},
129 | "outputs": [],
130 | "source": [
131 | "streamer.start_streaming(variables=[\"pot1\", \"pot2\"])\n",
132 | "streamer.plot_data(x_var=\"pot1\", y_vars=[\"pot1\", \"pot2\"], y_range=[0, 1], rollover=10000)"
133 | ]
134 | },
135 | {
136 | "cell_type": "markdown",
137 | "metadata": {},
138 | "source": [
139 | "You can stop streaming the values of potentiometer 1 and 2 by calling `streamer.stop_streaming(variables=[\"pot1\", \"pot2\"])`. You can also call `streamer.stop_streaming()` which will stop streaming all the available variables in the watcher (in this case, both `pot1` and `pot2`)."
140 | ]
141 | },
142 | {
143 | "cell_type": "code",
144 | "execution_count": null,
145 | "metadata": {},
146 | "outputs": [],
147 | "source": [
148 | "streamer.stop_streaming()\n"
149 | ]
150 | },
151 | {
152 | "cell_type": "markdown",
153 | "metadata": {},
154 | "source": [
155 | "### Using `.wait` to stream data for a fixed amount of time\n",
156 | "You can use the `.wait` method to stream data for a fixed amount of time. Note: you need to use `.wait` method instead of `time.sleep`, since the latter pauses the entire program (including the streaming tasks running in the background)."
157 | ]
158 | },
159 | {
160 | "cell_type": "code",
161 | "execution_count": null,
162 | "metadata": {},
163 | "outputs": [],
164 | "source": [
165 | "streamer.start_streaming(variables=[\"pot2\"])\n",
166 | "streamer.plot_data(x_var=\"pot2\", y_vars=[\"pot2\"], y_range=[0, 1])\n",
167 | "streamer.wait(10)\n",
168 | "streamer.stop_streaming()"
169 | ]
170 | },
171 | {
172 | "cell_type": "markdown",
173 | "metadata": {},
174 | "source": [
175 | "### Scheduling streaming sessions\n",
176 | "You can schedule a streaming session to start and stop at a specific time using the `schedule_streaming()` method. This method takes the same arguments as `start_streaming()`, but it also takes a `timestamps` and `durations` argument."
177 | ]
178 | },
179 | {
180 | "cell_type": "code",
181 | "execution_count": null,
182 | "metadata": {},
183 | "outputs": [],
184 | "source": [
185 | "latest_timestamp = streamer.get_latest_timestamp() # get the latest timestamp\n",
186 | "sample_rate = streamer.sample_rate # get the sample rate\n",
187 | "start_timestamp = latest_timestamp + sample_rate # start streaming 1 second after the latest timestamp\n",
188 | "duration = sample_rate # stream for 2 seconds\n",
189 | "\n",
190 | "streamer.schedule_streaming(\n",
191 | " variables=[\"pot1\", \"pot2\"],\n",
192 | " timestamps=[start_timestamp, start_timestamp],\n",
193 | " durations=[duration, duration],\n",
194 | " saving_enabled=True)"
195 | ]
196 | },
197 | {
198 | "cell_type": "markdown",
199 | "metadata": {},
200 | "source": [
201 | "### Note on streaming variables assigned at low frequency rates\n",
202 | "The data buffers sent from Bela have fixed sizes. The buffers will only be sent when they are full, unless you use the streaming with scheduling feature (explained below). If the variables you are streaming are assigned at too low rates, these buffers will take too long to fill up and the data will be either sent to python with a delay or not sent at all (if the buffer is never filled). For example, floats using dense timestamping are sent in buffers of 1024 values. If the float variable is assigned once every 12 milliseconds, filling a buffer will take 1024/(1/0.012) = 12.3 seconds. \n",
203 | "Hence, the streaming mode is not ideal for variables assigned at low rates, but rather for variables that are assigned quite frequently (e.g. at audio rate). If you want to stream variables that are assigned at lower rates, you can use the streaming with scheduling feature, or monitor or log the variable instead."
204 | ]
205 | },
206 | {
207 | "cell_type": "markdown",
208 | "metadata": {},
209 | "source": [
210 | "### Retrieving the data\n",
211 | "You can access the data streamed in `streamer.streaming_buffers_data`. We can use the pandas data manipulation library for printing the data onto a table:"
212 | ]
213 | },
214 | {
215 | "cell_type": "code",
216 | "execution_count": null,
217 | "metadata": {},
218 | "outputs": [],
219 | "source": [
220 | "df = pd.DataFrame(streamer.streaming_buffers_data[\"pot2\"])\n",
221 | "df.head() # head shows only the first 5 rows"
222 | ]
223 | },
224 | {
225 | "cell_type": "markdown",
226 | "metadata": {},
227 | "source": [
228 | "As you can see, `streaming_buffers_data` only retrieves the variable values but not its timestamps. If you want to retrieve the timestamps, you can access `streaming_buffers_queue[\"pot2\"]`. This will return a list in which every item is a timestamped buffer:"
229 | ]
230 | },
231 | {
232 | "cell_type": "code",
233 | "execution_count": null,
234 | "metadata": {},
235 | "outputs": [],
236 | "source": [
237 | "streamer.streaming_buffers_queue[\"pot2\"][0]"
238 | ]
239 | },
240 | {
241 | "cell_type": "markdown",
242 | "metadata": {},
243 | "source": [
244 | "In the buffer `ref_timestamp` corresponds to the timestamp of the first value of the buffer (`streaming_buffers_queue[\"pot2\"][0][\"data\"][0]`). If the Bela Watcher is ticked once per analog frame (as it is the case in the `potentiometer` code) and the variable `pot2` is assigned also once per analog frame, the timestamps of the rest of the values in the data buffer correspond to the increasing timestamps:"
245 | ]
246 | },
247 | {
248 | "cell_type": "code",
249 | "execution_count": null,
250 | "metadata": {},
251 | "outputs": [],
252 | "source": [
253 | "data_timestamps = []\n",
254 | "data_values = []\n",
255 | "\n",
256 | "def flatten_buffers_queue(_buffer_queue):\n",
257 | " for _buffer in _buffer_queue:\n",
258 | " ref_timestamp = _buffer[\"ref_timestamp\"]\n",
259 | " data_timestamps.extend([ref_timestamp + i for i in range(len(_buffer[\"data\"]))])\n",
260 | " data_values.extend(_buffer[\"data\"])\n",
261 | " \n",
262 | " return data_timestamps, data_values\n",
263 | "\n",
264 | "data_timestamps, data_values = flatten_buffers_queue(streamer.streaming_buffers_queue[\"pot2\"])\n",
265 | " \n",
266 | "df = pd.DataFrame({\"timestamp\": data_timestamps, \"value\": data_values})\n",
267 | "df.head()"
268 | ]
269 | },
270 | {
271 | "cell_type": "markdown",
272 | "metadata": {},
273 | "source": [
274 | "More advanced timestamping methods will be shown in the tutorial notebook `7_Sparse_timestamping.ipynb`\n",
275 | "\n",
276 | "There is a limited amount of data that is stored in the streamer. This quantity can be modified by changing the buffer queue length. The streamer receives the data in buffers of fixed length that get stored in a queue that also has a fixed length. You can calculate the maximum amount of data the streamer can store for each variable:\n",
277 | "\n",
278 | "note: `streamer.watcher_vars` returns information of the variables available in the watcher, that is, variables that have been defined within the Watcher class in the Bela code and that are available for streaming, monitoring or logging."
279 | ]
280 | },
281 | {
282 | "cell_type": "code",
283 | "execution_count": null,
284 | "metadata": {},
285 | "outputs": [],
286 | "source": [
287 | "print(f\"Buffer queue length: {streamer.streaming_buffers_queue_length}\")\n",
288 | "\n",
289 | "for var in streamer.watcher_vars: \n",
290 | " print(f'Variable: {var[\"name\"]}, buffer length: {var[\"data_length\"]}, max data stored in streamer: {var[\"data_length\"]*streamer.streaming_buffers_queue_length}')"
291 | ]
292 | },
293 | {
294 | "cell_type": "markdown",
295 | "metadata": {},
296 | "source": [
297 | "You can also modify the queue length:"
298 | ]
299 | },
300 | {
301 | "cell_type": "code",
302 | "execution_count": null,
303 | "metadata": {},
304 | "outputs": [],
305 | "source": [
306 | "streamer.streaming_buffers_queue_length = 10"
307 | ]
308 | },
309 | {
310 | "cell_type": "markdown",
311 | "metadata": {},
312 | "source": [
313 | "### Saving the streamed data\n",
314 | "Every time you start a new streaming session (e.g. you call `start_streaming()` or `stream_n_values()`), the data stored in the streamer from the previous streaming session will be deleted. If you want to store the streamed data, you can do so by setting `saving_enabled=True` when calling `start_streaming()` or `stream_n_values()`:"
315 | ]
316 | },
317 | {
318 | "cell_type": "code",
319 | "execution_count": null,
320 | "metadata": {},
321 | "outputs": [],
322 | "source": [
323 | "streamer.start_streaming(variables=[var[\"name\"] for var in streamer.watcher_vars], saving_enabled=True, saving_filename=\"test.txt\")\n",
324 | "streamer.wait(3)\n",
325 | "streamer.stop_streaming()"
326 | ]
327 | },
328 | {
329 | "cell_type": "markdown",
330 | "metadata": {},
331 | "source": [
332 | "You can load the data stored using the `load_data_from_file` method. This will return the buffers queue. Again, we can flatten it using the `flatten_buffers_queue()` function we defined above:"
333 | ]
334 | },
335 | {
336 | "cell_type": "code",
337 | "execution_count": null,
338 | "metadata": {},
339 | "outputs": [],
340 | "source": [
341 | "data_timestamps, data_values = flatten_buffers_queue(streamer.load_data_from_file(\"pot1_test.txt\"))\n",
342 | "\n",
343 | "df=pd.DataFrame({\"timestamp\": data_timestamps, \"value\": data_values})\n",
344 | "df.head()"
345 | ]
346 | }
347 | ],
348 | "metadata": {
349 | "kernelspec": {
350 | "display_name": "pybela-2uXYSGIe",
351 | "language": "python",
352 | "name": "python3"
353 | },
354 | "language_info": {
355 | "codemirror_mode": {
356 | "name": "ipython",
357 | "version": 3
358 | },
359 | "file_extension": ".py",
360 | "mimetype": "text/x-python",
361 | "name": "python",
362 | "nbconvert_exporter": "python",
363 | "pygments_lexer": "ipython3",
364 | "version": "3.9.19"
365 | }
366 | },
367 | "nbformat": 4,
368 | "nbformat_minor": 4
369 | }
370 |
--------------------------------------------------------------------------------
/tutorials/notebooks/2_Streamer-Bela-to-python-advanced.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# pybela Tutorial 2: Streamer – Bela to python advanced\n",
8 | "This notebook is a tutorial for the Streamer class in the pybela python library. You can use the Streamer to stream data from Bela to python or vice versa. \n",
9 | "\n",
10 | "In this tutorial we will be looking at more advanced features to send data from Bela to python. \n",
11 | "\n",
12 | "The complete documentation for the pybela library can be found in [https://belaplatform.github.io/pybela/](https://belaplatform.github.io/pybela/).\n",
13 | "\n",
14 | "If you didn't do it in the previous tutorial, copy the `bela-code/potentiometers` project onto Bela. If your Bela is connected to your laptop, you can run the cell below:"
15 | ]
16 | },
17 | {
18 | "cell_type": "code",
19 | "execution_count": null,
20 | "metadata": {},
21 | "outputs": [],
22 | "source": [
23 | "!rsync -rvL ../bela-code/potentiometers root@bela.local:Bela/projects"
24 | ]
25 | },
26 | {
27 | "cell_type": "markdown",
28 | "metadata": {},
29 | "source": [
30 | "Then you can compile and run the project using either the IDE or by running the following command in the Terminal:\n",
31 | "```bash\n",
32 | "ssh root@bela.local \"make -C Bela stop Bela PROJECT=potentiometers run\" \n",
33 | "```\n",
34 | "(Running this on a jupyter notebook will block the cell until the program is stopped on Bela.) You will also need to connect two potentiometers to Bela analog inputs 0 and 1. Instructions on how to do so and some details on the Bela code are given in the notebook `1_Streamer-Bela-to-python-basics.ipynb`.\n",
35 | "\n",
36 | "First, we need to import the pybela library, create a Streamer object and connect to Bela."
37 | ]
38 | },
39 | {
40 | "cell_type": "code",
41 | "execution_count": null,
42 | "metadata": {},
43 | "outputs": [],
44 | "source": [
45 | "from pybela import Streamer\n",
46 | "\n",
47 | "streamer = Streamer()\n",
48 | "streamer.connect()\n",
49 | "\n",
50 | "variables = [\"pot1\", \"pot2\"]"
51 | ]
52 | },
53 | {
54 | "cell_type": "markdown",
55 | "metadata": {},
56 | "source": [
57 | "### Streaming a fixed number of values\n",
58 | "You can can use the method `stream_n_values` to stream a fixed number of values of a variable. "
59 | ]
60 | },
61 | {
62 | "cell_type": "code",
63 | "execution_count": null,
64 | "metadata": {},
65 | "outputs": [],
66 | "source": [
67 | "n_values = 1000\n",
68 | "streaming_buffer = streamer.stream_n_values(\n",
69 | " variables= variables, n_values=n_values)"
70 | ]
71 | },
72 | {
73 | "cell_type": "markdown",
74 | "metadata": {},
75 | "source": [
76 | "Since the data buffers received from Bela have a fixed size, unless the number of values `n_values` is a multiple of the data buffers size, the streamer will always return a few more values than asked for."
77 | ]
78 | },
79 | {
80 | "cell_type": "code",
81 | "execution_count": null,
82 | "metadata": {},
83 | "outputs": [],
84 | "source": [
85 | "_vars = streamer.watcher_vars\n",
86 | "for var in _vars:\n",
87 | " print(f'Variable: {var[\"name\"]}, buffer length: {var[\"data_length\"]}, number of streamed values: {len(streamer.streaming_buffers_data[var[\"name\"]])}')"
88 | ]
89 | },
90 | {
91 | "cell_type": "markdown",
92 | "metadata": {},
93 | "source": [
94 | "### Scheduling streaming sessions\n",
95 | "You can schedule a streaming session to start and stop at a specific time using the `schedule_streaming()` method. This method takes the same arguments as `start_streaming()`, but it also takes a `timestamps` and `durations` argument."
96 | ]
97 | },
98 | {
99 | "cell_type": "code",
100 | "execution_count": null,
101 | "metadata": {},
102 | "outputs": [],
103 | "source": [
104 | "latest_timestamp = streamer.get_latest_timestamp() # get the latest timestamp\n",
105 | "sample_rate = streamer.sample_rate # get the sample rate\n",
106 | "start_timestamp = latest_timestamp + sample_rate # start streaming 1 second after the latest timestamp\n",
107 | "duration = sample_rate # stream for 2 seconds\n",
108 | "\n",
109 | "streamer.schedule_streaming(\n",
110 | " variables=variables,\n",
111 | " timestamps=[start_timestamp, start_timestamp],\n",
112 | " durations=[duration, duration],\n",
113 | " saving_enabled=True)"
114 | ]
115 | },
116 | {
117 | "cell_type": "markdown",
118 | "metadata": {},
119 | "source": [
120 | "### On-buffer and on-block callbacks\n",
121 | "Up until now, we have been streaming data for a period of time and processed the data once the streaming has finished. However, you can also process the data as it is being received. You can do this by passing a callback function to the `on_buffer` or `on_block` arguments of the `start_streaming()` method. \n",
122 | "\n",
123 | "The `on_buffer` callback will be called every time a buffer is received from Bela. We will need to define a callback function that takes one argument, the buffer. The Streamer will call that function every time it receives a buffer. You can also pass variables to the callback function by using the `callback_args` argument of the `start_streaming()` method. Let's see an example:"
124 | ]
125 | },
126 | {
127 | "cell_type": "code",
128 | "execution_count": null,
129 | "metadata": {},
130 | "outputs": [],
131 | "source": [
132 | "timestamps = {var: [] for var in variables}\n",
133 | "buffers = {var: [] for var in variables}\n",
134 | "\n",
135 | "def callback(buffer, timestamps, buffers):\n",
136 | " print(\"Buffer received\")\n",
137 | " \n",
138 | " _var = buffer[\"name\"]\n",
139 | " timestamps[_var].append(\n",
140 | " buffer[\"buffer\"][\"ref_timestamp\"])\n",
141 | " buffers[_var].append(buffer[\"buffer\"][\"data\"])\n",
142 | " \n",
143 | " print(_var, timestamps[_var][-1])\n",
144 | "\n",
145 | "streamer.start_streaming(\n",
146 | " variables, saving_enabled=False, on_buffer_callback=callback, callback_args=(timestamps, buffers))\n",
147 | "\n",
148 | "streamer.wait(2)\n",
149 | "\n",
150 | "streamer.stop_streaming()"
151 | ]
152 | },
153 | {
154 | "cell_type": "markdown",
155 | "metadata": {},
156 | "source": [
157 | "Let's now look at the `on_block`callback. We call block to a group of buffers. If you are streaming two variables, `pot1` and `pot2`, a block of buffers will contain a buffer for `pot1` and a buffer for `pot2`. If `pot1` and `pot2` have the same buffer size and they are being streamed at the same rate, `pot1` and `pot2` will be aligned in time. This is useful if you are streaming multiple variables and you want to process them together. \n",
158 | "\n",
159 | "The `on_block` callback will be called every time a block of buffers is received from Bela. We will need to define a callback function that takes one argument, the block. The Streamer will call that function every time it receives a block of buffers. Let's see an example:"
160 | ]
161 | },
162 | {
163 | "cell_type": "code",
164 | "execution_count": null,
165 | "metadata": {},
166 | "outputs": [],
167 | "source": [
168 | "timestamps = {var: [] for var in variables}\n",
169 | "buffers = {var: [] for var in variables}\n",
170 | "\n",
171 | "def callback(block, timestamps, buffers):\n",
172 | " print(\"Block received\")\n",
173 | " \n",
174 | " for buffer in block:\n",
175 | " var = buffer[\"name\"]\n",
176 | " timestamps[var].append(buffer[\"buffer\"][\"ref_timestamp\"])\n",
177 | " buffers[var].append(buffer[\"buffer\"][\"data\"])\n",
178 | "\n",
179 | " print(var, timestamps[var][-1])\n",
180 | " \n",
181 | "streamer.start_streaming(\n",
182 | " variables, saving_enabled=False, on_block_callback=callback, callback_args=(timestamps, buffers))\n",
183 | "\n",
184 | "streamer.wait(2)\n",
185 | "\n",
186 | "streamer.stop_streaming()"
187 | ]
188 | }
189 | ],
190 | "metadata": {
191 | "kernelspec": {
192 | "display_name": "pybela-2uXYSGIe",
193 | "language": "python",
194 | "name": "python3"
195 | },
196 | "language_info": {
197 | "codemirror_mode": {
198 | "name": "ipython",
199 | "version": 3
200 | },
201 | "file_extension": ".py",
202 | "mimetype": "text/x-python",
203 | "name": "python",
204 | "nbconvert_exporter": "python",
205 | "pygments_lexer": "ipython3",
206 | "version": "3.9.19"
207 | }
208 | },
209 | "nbformat": 4,
210 | "nbformat_minor": 2
211 | }
212 |
--------------------------------------------------------------------------------
/tutorials/notebooks/3_Streamer-python-to-Bela.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# pybela Tutorial 3: Streamer – python to Bela\n",
8 | "This notebook is a tutorial for the Streamer class in the pybela python library. You can use the Streamer to stream data from Bela to python or viceversa. The complete documentation for the pybela library can be found in [https://belaplatform.github.io/pybela/](https://belaplatform.github.io/pybela/).\n",
9 | "\n",
10 | "In this tutorial we will be looking at sending data from python to Bela. There is only one method available in the Streamer class for this purpose: `send_buffer()`. This method sends a buffer of a certain type and size to Bela. \n",
11 | "\n",
12 | "To run this tutorial, first copy the `bela-code/bela2python2bela` project onto Bela. If your Bela is connected to your laptop, you can run the cell below:"
13 | ]
14 | },
15 | {
16 | "cell_type": "code",
17 | "execution_count": null,
18 | "metadata": {},
19 | "outputs": [],
20 | "source": [
21 | "!rsync -rvL ../bela-code/bela2python2bela root@bela.local:Bela/projects"
22 | ]
23 | },
24 | {
25 | "cell_type": "markdown",
26 | "metadata": {},
27 | "source": [
28 | "Then you can compile and run the project using either the IDE or by running the following command in the Terminal:\n",
29 | "```bash\n",
30 | "ssh root@bela.local \"make -C Bela stop Bela PROJECT=bela2python2bela run\" \n",
31 | "```\n",
32 | "(Running this on a jupyter notebook will block the cell until the program is stopped on Bela.) \n",
33 | "\n",
34 | "This program expects two analog signals in channels 0 and 1, you can keep using the potentiometer setup from the previous tutorials (check the schematic in `1_Streamer-Bela-to-python.ipynb`)\n",
35 | "\n",
36 | "In this example we will be sending the values of the two potentiometers from Bela to python. Once received in python, we will send them immediately back to Bela. The values received in Bela will be used to modulate the amplitude of two sine waves. It is admittedly an overly complicated way to modulate two sine waves in Bela, as you could of course use the potentiometer values directly, without having to send them to python and back. However, this example can serve as a template for more complex applications where you can process the data in python before sending it back to Bela. \n",
37 | "\n",
38 | "## Understanding the Bela code\n",
39 | "If you are not familiar with auxiliary tasks and circular buffers, we recommend you follow first [Lesson 11](https://youtu.be/xQBftd7WNY8?si=ns6ojYnfQ_GVtCQI) and [Lesson 17](https://youtu.be/2uyWn8P0CVg?si=Ymy-NN_HKS-Q3xL0) of the C++ Real-Time Audio Programming with Bela course. \n",
40 | "\n",
41 | "Let's first take a look at the Bela code. The `setup()` function initializes the Bela program and some necessary variables. First, we set up the Watcher with the `Bela_getDefaultWatcherManager()` function. We then calculate the inverse of some useful variables (multiplying by the inverse is faster than dividing, so we precompute the inverse in `setup` and use it later in `render`). We then initialize the GUI buffers (these are the internal buffers Bela uses to receive the data) and the `circularBuffers`. The `circularBuffers` are used to store the parsed data from the GUI buffers, and are the variables we will use in `render` to access the data we have sent from python. We also set up the `binaryDataCallback` function, which will be called when Bela receives a buffer from python. \n",
42 | "\n",
43 | "\n",
44 | "```cpp\n",
45 | "bool setup(BelaContext* context, void* userData) {\n",
46 | "\n",
47 | " Bela_getDefaultWatcherManager()->getGui().setup(context->projectName);\n",
48 | " Bela_getDefaultWatcherManager()->setup(context->audioSampleRate); // set sample rate in watcher\n",
49 | "\n",
50 | " gAudioFramesPerAnalogFrame = context->audioFrames / context->analogFrames;\n",
51 | " gInvAudioFramesPerAnalogFrame = 1.0 / gAudioFramesPerAnalogFrame;\n",
52 | " gInverseSampleRate = 1.0 / context->audioSampleRate;\n",
53 | "\n",
54 | " // initialize the Gui buffers and circular buffers\n",
55 | " for (int i = 0; i < NUM_OUTPUTS; ++i) {\n",
56 | " Bela_getDefaultWatcherManager()->getGui().setBuffer('f', MAX_EXPECTED_BUFFER_SIZE);\n",
57 | " circularBuffers[i].resize(circularBufferSize, 0.0f);\n",
58 | " // the write index is given some \"advantage\" (prefillSize) so that the read pointer does not catch up the write pointer\n",
59 | " circularBufferWriteIndex[i] = prefillSize % circularBufferSize;\n",
60 | " }\n",
61 | "\n",
62 | " Bela_getDefaultWatcherManager()->getGui().setBinaryDataCallback(binaryDataCallback);\n",
63 | "\n",
64 | " // vars and preparation for parsing the received buffer\n",
65 | " receivedBufferHeaderSize = sizeof(receivedBuffer.bufferId) + sizeof(receivedBuffer.bufferType) + sizeof(receivedBuffer.bufferLen) + sizeof(receivedBuffer.empty);\n",
66 | " totalReceivedCount = 0;\n",
67 | " receivedBuffer.bufferData.reserve(MAX_EXPECTED_BUFFER_SIZE);\n",
68 | "\n",
69 | " return true;\n",
70 | "}\n",
71 | "```\n",
72 | "\n",
73 | "Let's now take a look at the `render()` function. The render function is called once per audio block, so inside of it we iterate over the audio blocks. Since the potentiometers are analog signals, and in Bela the analog inputs are typically sampled at a lower rate than the audio, we read the potentiometers once every 2 audio frames (in the code, `gAudioFramesPerAnalogFrame` is equal to 2 if you are using the default 8 audio channels). Since the variables `pot1` and `pot2` are in the Watcher, these will be streamed to python if we run `start_streaming()` in python.\n",
74 | "\n",
75 | "Next, we check if the variable `totalReceivedCount` is greater than 0, which means that we have received at least a buffer from python. If we have received buffers and the read pointer has not caught up with the write pointer, we advance the read pointer in the circular buffer. The reason why we check if we have received a buffer first, is because we don't want to advance the read pointer if we haven't received any data yet, as then the read pointer would catch up with the write pointer. \n",
76 | "\n",
77 | "Finally, we read the values from the circular buffer and use them to modulate the amplitude of two sine waves. We then write the output to the audio channels.\n",
78 | "\n",
79 | "\n",
80 | "\n",
81 | "```cpp\n",
82 | "\n",
83 | "void render(BelaContext* context, void* userData) {\n",
84 | " for (unsigned int n = 0; n < context->audioFrames; n++) {\n",
85 | " uint64_t frames = context->audioFramesElapsed + n;\n",
86 | "\n",
87 | " if (gAudioFramesPerAnalogFrame && !(n % gAudioFramesPerAnalogFrame)) {\n",
88 | " Bela_getDefaultWatcherManager()->tick(frames * gInvAudioFramesPerAnalogFrame); // watcher timestamps\n",
89 | "\n",
90 | " // read sensor values and put them in the watcher\n",
91 | " pot1 = analogRead(context, n / gAudioFramesPerAnalogFrame, gPot1Ch);\n",
92 | " pot2 = analogRead(context, n / gAudioFramesPerAnalogFrame, gPot2Ch);\n",
93 | "\n",
94 | " // read the values sent from python (they're in the circular buffer)\n",
95 | " for (unsigned int i = 0; i < NUM_OUTPUTS; i++) {\n",
96 | "\n",
97 | " if (totalReceivedCount > 0 && (circularBufferReadIndex[i] + 1) % circularBufferSize != circularBufferWriteIndex[i]) {\n",
98 | " circularBufferReadIndex[i] = (circularBufferReadIndex[i] + 1) % circularBufferSize;\n",
99 | " } else if (totalReceivedCount > 0) {\n",
100 | " rt_printf(\"The read pointer has caught the write pointer up in buffer %d – try increasing prefillSize\\n\", i);\n",
101 | " }\n",
102 | " }\n",
103 | " }\n",
104 | "\n",
105 | " float amp1 = circularBuffers[0][circularBufferReadIndex[0]];\n",
106 | " float amp2 = circularBuffers[1][circularBufferReadIndex[1]];\n",
107 | "\n",
108 | " float out = amp1 * sinf(gPhase1) + amp2 * sinf(gPhase2);\n",
109 | "\n",
110 | " for (unsigned int channel = 0; channel < context->audioOutChannels; channel++) {\n",
111 | " audioWrite(context, n, channel, out);\n",
112 | " }\n",
113 | "\n",
114 | " gPhase1 += 2.0f * (float)M_PI * gFrequency1 * gInverseSampleRate;\n",
115 | " if (gPhase1 > M_PI)\n",
116 | " gPhase1 -= 2.0f * (float)M_PI;\n",
117 | " gPhase2 += 2.0f * (float)M_PI * gFrequency2 * gInverseSampleRate;\n",
118 | " if (gPhase2 > M_PI)\n",
119 | " gPhase2 -= 2.0f * (float)M_PI;\n",
120 | "\n",
121 | " }\n",
122 | "}\n",
123 | "```\n",
124 | "\n",
125 | "Let's now run the python code:"
126 | ]
127 | },
128 | {
129 | "cell_type": "code",
130 | "execution_count": null,
131 | "metadata": {},
132 | "outputs": [],
133 | "source": [
134 | "from pybela import Streamer\n",
135 | "streamer = Streamer()\n",
136 | "streamer.connect()\n",
137 | "\n",
138 | "variables = [\"pot1\", \"pot2\"]"
139 | ]
140 | },
141 | {
142 | "cell_type": "markdown",
143 | "metadata": {},
144 | "source": [
145 | "The `send_buffer` function takes 4 arguments: the buffer id, the type of the data that goes in the buffer, the buffer length and the buffer data. Since we will be sending back the buffers we receive from Bela, we can get the type and length of the buffer through the streamer:"
146 | ]
147 | },
148 | {
149 | "cell_type": "code",
150 | "execution_count": null,
151 | "metadata": {},
152 | "outputs": [],
153 | "source": [
154 | "buffer_type = streamer.get_prop_of_var(\"pot1\", \"type\")\n",
155 | "buffer_length = streamer.get_prop_of_var(\"pot1\", \"data_length\")\n",
156 | "\n",
157 | "buffer_type, buffer_length\n"
158 | ]
159 | },
160 | {
161 | "cell_type": "markdown",
162 | "metadata": {},
163 | "source": [
164 | "Here we will be using the `block_callback` instead of the `buffer_callback`, as the `block` callback is more efficient. It should be noted that we are receiving and sending blocks of data every 1024/22050 = 0.05 seconds, and the maximum latency is given by the `prefillSize` variable in the Bela code (which is set to 2.5*1024/22050 = 0.12 seconds), so using functions is crucial to meet the real-time deadlines."
165 | ]
166 | },
167 | {
168 | "cell_type": "code",
169 | "execution_count": null,
170 | "metadata": {},
171 | "outputs": [],
172 | "source": [
173 | "def callback(block):\n",
174 | " \n",
175 | " for buffer in block:\n",
176 | " \n",
177 | " _var = buffer[\"name\"]\n",
178 | " timestamp = buffer[\"buffer\"][\"ref_timestamp\"]\n",
179 | " data = buffer[\"buffer\"][\"data\"]\n",
180 | " \n",
181 | " buffer_id = 0 if _var == \"pot1\" else 1\n",
182 | "\n",
183 | " print(buffer_id, timestamp)\n",
184 | " # do some data processing here...\n",
185 | " processed_data = data\n",
186 | " \n",
187 | " # send processed_data back\n",
188 | " streamer.send_buffer(buffer_id, buffer_type,\n",
189 | " buffer_length, processed_data)\n",
190 | "\n",
191 | "streamer.start_streaming(\n",
192 | " variables, saving_enabled=False, on_block_callback=callback)"
193 | ]
194 | },
195 | {
196 | "cell_type": "markdown",
197 | "metadata": {},
198 | "source": [
199 | "If you plug in your headphones to the audio output of Bela, you should hear two sine waves modulated by the potentiometers. The modulation (the amplitude change) is given by the value sent by python, not the analog input directly on Bela. As mentioned before, this is an overly complicated way to modulate two sine waves, but it can serve as a template for more complex applications where you can process the data in python before sending it back to Bela."
200 | ]
201 | },
202 | {
203 | "cell_type": "markdown",
204 | "metadata": {},
205 | "source": [
206 | "..."
207 | ]
208 | },
209 | {
210 | "cell_type": "code",
211 | "execution_count": null,
212 | "metadata": {},
213 | "outputs": [],
214 | "source": [
215 | "streamer.stop_streaming()"
216 | ]
217 | }
218 | ],
219 | "metadata": {
220 | "kernelspec": {
221 | "display_name": "pybela-2uXYSGIe",
222 | "language": "python",
223 | "name": "python3"
224 | },
225 | "language_info": {
226 | "codemirror_mode": {
227 | "name": "ipython",
228 | "version": 3
229 | },
230 | "file_extension": ".py",
231 | "mimetype": "text/x-python",
232 | "name": "python",
233 | "nbconvert_exporter": "python",
234 | "pygments_lexer": "ipython3",
235 | "version": "3.9.19"
236 | }
237 | },
238 | "nbformat": 4,
239 | "nbformat_minor": 4
240 | }
241 |
--------------------------------------------------------------------------------
/tutorials/notebooks/4_Monitor.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# pybela tutorial 4: Monitor\n",
8 | "This tutorial expects the `potentiometers` project to be running on Bela. If the Bela is connected to your laptop, you can run the cell below to copy the `potentiometers` code with the `Watcher` library onto your Bela:"
9 | ]
10 | },
11 | {
12 | "cell_type": "code",
13 | "execution_count": null,
14 | "metadata": {},
15 | "outputs": [],
16 | "source": [
17 | "!rsync -rvL ../bela-code/potentiometers root@bela.local:Bela/projects"
18 | ]
19 | },
20 | {
21 | "cell_type": "markdown",
22 | "metadata": {},
23 | "source": [
24 | "Then you can compile and run the project using either the IDE or by running the following command in the Terminal:\n",
25 | "```bash\n",
26 | "ssh root@bela.local \"make -C Bela stop Bela PROJECT=potentiometers run\" \n",
27 | "```\n",
28 | "(Running this on a jupyter notebook will block the cell until the program is stopped on Bela.)\n",
29 | "\n",
30 | "You will also need to connect two potentiometers to Bela analog inputs 0 and 1. Instructions on how to do so and some details on the Bela code are given in the notebook `1_Streamer-Bela-to-python-basics.ipynb`. The complete documentation for the pybela library can be found in [https://belaplatform.github.io/pybela/](https://belaplatform.github.io/pybela/).\n",
31 | "\n",
32 | "This notebook is a tutorial for the Monitor class in the pybela python library. The monitor allows you to \"take a look\" at variables in your Bela code. By taking a look we mean either requesting a single value (*what value does `pot1` have right now?*) or sampling the value of a variable, that is, getting a value every number of frames (*can you tell me the value of `pot1` every 1000 frames?*). The monitor can be useful to calibrate sensors or, in general, debug your Bela code. \n",
33 | "\n",
34 | "The Monitor class inherits from the Streamer, so you can use it in a similar way, with the only difference that you will need to specify a monitoring period. Let's take a look at an example:"
35 | ]
36 | },
37 | {
38 | "cell_type": "code",
39 | "execution_count": null,
40 | "metadata": {},
41 | "outputs": [],
42 | "source": [
43 | "import pandas as pd\n",
44 | "from pybela import Monitor"
45 | ]
46 | },
47 | {
48 | "cell_type": "markdown",
49 | "metadata": {},
50 | "source": [
51 | "As with the streamer, we need to instantiate the Monitor class and run the `connect()` method to establish the websocket connection with Bela. If the connection fails, make sure Bela is plugged in to your laptop and that the `potentiometer` project is running on Bela. "
52 | ]
53 | },
54 | {
55 | "cell_type": "code",
56 | "execution_count": null,
57 | "metadata": {},
58 | "outputs": [],
59 | "source": [
60 | "monitor = Monitor()\n",
61 | "monitor.connect()"
62 | ]
63 | },
64 | {
65 | "cell_type": "markdown",
66 | "metadata": {},
67 | "source": [
68 | "### Using asyncio to monitor data for a fixed amount of time\n",
69 | "\n",
70 | "The monitor works in a very similar way to the Streamer but with a key difference: it doesn't continuously stream the values of the variables. Instead, it samples and streams these values periodically based on the given period. In this example, we will be monitoring two variables, `pot1` and `pot2` with a period 1000. \n",
71 | "\n",
72 | "As with the Streamer example, we can use asyncio to time the monitoring session:"
73 | ]
74 | },
75 | {
76 | "cell_type": "code",
77 | "execution_count": null,
78 | "metadata": {},
79 | "outputs": [],
80 | "source": [
81 | "monitor.start_monitoring(variables= [\"pot1\", \"pot2\"], \n",
82 | " periods= [1000,2000])\n",
83 | "\n",
84 | "monitor.wait(2)\n",
85 | "\n",
86 | "monitored_values = monitor.stop_monitoring()"
87 | ]
88 | },
89 | {
90 | "cell_type": "markdown",
91 | "metadata": {},
92 | "source": [
93 | "### Retrieving the data\n",
94 | "\n",
95 | "`stop_monitoring` returns a dictionary with the monitored values and its timestamps. You can also access this data in `monitor.values`. Note that the timestamps are spaced by the period as we requested."
96 | ]
97 | },
98 | {
99 | "cell_type": "code",
100 | "execution_count": null,
101 | "metadata": {},
102 | "outputs": [],
103 | "source": [
104 | "df = pd.DataFrame(monitored_values[\"pot1\"])\n",
105 | "df.head()\n"
106 | ]
107 | },
108 | {
109 | "cell_type": "code",
110 | "execution_count": null,
111 | "metadata": {},
112 | "outputs": [],
113 | "source": [
114 | "df = pd.DataFrame(monitored_values[\"pot2\"])\n",
115 | "df.head()"
116 | ]
117 | },
118 | {
119 | "cell_type": "markdown",
120 | "metadata": {},
121 | "source": [
122 | "A note regarding the periods: in this example, the returned timestamps correspond to the analog frames elapsed in the Bela code. Let's take a look at the `render` loop in the `potentiometers` Bela project:\n",
123 | "\n",
124 | "```cpp\n",
125 | "void render(BelaContext *context, void *userData)\n",
126 | "{\n",
127 | "\tfor(unsigned int n = 0; n < context->audioFrames; n++) {\n",
128 | "\t\tif(gAudioFramesPerAnalogFrame && !(n % gAudioFramesPerAnalogFrame)) {\n",
129 | "\t\t\t\n",
130 | "\t\t\tuint64_t frames = context->audioFramesElapsed/gAudioFramesPerAnalogFrame + n/gAudioFramesPerAnalogFrame;\n",
131 | "\t\t\tBela_getDefaultWatcherManager()->tick(frames); // watcher timestamps\n",
132 | "\t\t\t\n",
133 | "\t\t\tpot1 = analogRead(context, n/gAudioFramesPerAnalogFrame, gPot1Ch);\n",
134 | "\t\t\tpot2 = analogRead(context, n/gAudioFramesPerAnalogFrame, gPot2Ch);\n",
135 | "\t\t\t\n",
136 | "\t\t}\n",
137 | "\t}\n",
138 | "}\n",
139 | "```\n",
140 | "\n",
141 | "AS you can see, we are \"ticking\" the Bela watcher once per every analog frame. When we request variable `pot1` with a period 1000, we will be getting the value of `pot1` every 1000 analog frames. For more advanced timestamping methods, you can check the `4_Sparse_timestamping.ipynb` notebook."
142 | ]
143 | },
144 | {
145 | "cell_type": "markdown",
146 | "metadata": {},
147 | "source": [
148 | "### Monitoring a fixed number of values\n",
149 | "Alternatively, you can ask the monitor to monitor a variable for a fixed number of values. Another difference between the Streamer and the Monitor is that the Monitor will monitor exactly `n_values`. This is because in the monitor, the data buffers sent by Bela have only one timestamped value each, whilst in the Streamer, the buffers have a larger number of data points (which depends on the timestamping method and the data type). Let's see an example:"
150 | ]
151 | },
152 | {
153 | "cell_type": "code",
154 | "execution_count": null,
155 | "metadata": {},
156 | "outputs": [],
157 | "source": [
158 | "monitored_data = monitor.monitor_n_values(variables= [\"pot1\", \"pot2\"],\n",
159 | " periods= [1000,2000],\n",
160 | " n_values= 10)"
161 | ]
162 | },
163 | {
164 | "cell_type": "markdown",
165 | "metadata": {},
166 | "source": [
167 | "We can check that even when the periods for `pot1` and `pot2` where different, we monitored exactly 10 values for each:"
168 | ]
169 | },
170 | {
171 | "cell_type": "code",
172 | "execution_count": null,
173 | "metadata": {},
174 | "outputs": [],
175 | "source": [
176 | "for var in [\"pot1\", \"pot2\"]:\n",
177 | " print(f\"Monitored values for {var}: {len(monitored_data[var])}\")"
178 | ]
179 | },
180 | {
181 | "cell_type": "code",
182 | "execution_count": null,
183 | "metadata": {},
184 | "outputs": [],
185 | "source": [
186 | "df = pd.DataFrame(monitor.values[\"pot1\"])\n",
187 | "\n",
188 | "df.head()"
189 | ]
190 | },
191 | {
192 | "cell_type": "code",
193 | "execution_count": null,
194 | "metadata": {},
195 | "outputs": [],
196 | "source": [
197 | "df = pd.DataFrame(monitor.values[\"pot2\"])\n",
198 | "\n",
199 | "df.head()"
200 | ]
201 | },
202 | {
203 | "cell_type": "markdown",
204 | "metadata": {},
205 | "source": [
206 | "### Saving monitored data\n",
207 | "You can also store the monitored values by passing `saving_enabled=True` to either `start_monitoring()` or `monitor_n_values()`. "
208 | ]
209 | },
210 | {
211 | "cell_type": "code",
212 | "execution_count": null,
213 | "metadata": {},
214 | "outputs": [],
215 | "source": [
216 | "monitor.start_monitoring(variables= [\"pot1\", \"pot2\"],periods= [600,2000], saving_enabled=True)\n",
217 | "monitor.wait(2)\n",
218 | "monitor.stop_monitoring()"
219 | ]
220 | },
221 | {
222 | "cell_type": "markdown",
223 | "metadata": {},
224 | "source": [
225 | "You can load the data using `load_data_from_file()`:"
226 | ]
227 | },
228 | {
229 | "cell_type": "code",
230 | "execution_count": null,
231 | "metadata": {},
232 | "outputs": [],
233 | "source": [
234 | "pot1_saved_data = monitor.load_data_from_file(\"pot1_monitor.txt\")\n",
235 | "\n",
236 | "df = pd.DataFrame(pot1_saved_data)\n",
237 | "df.head()"
238 | ]
239 | },
240 | {
241 | "cell_type": "markdown",
242 | "metadata": {},
243 | "source": [
244 | "### Peeking at variables\n",
245 | "You can also use the monitor to \"peek\" at variables, that is, requesting a single value."
246 | ]
247 | },
248 | {
249 | "cell_type": "code",
250 | "execution_count": null,
251 | "metadata": {},
252 | "outputs": [],
253 | "source": [
254 | "peeked_values = monitor.peek() # peeks at all the available variables (otherwise specify)\n",
255 | "peeked_values[\"pot1\"]"
256 | ]
257 | }
258 | ],
259 | "metadata": {
260 | "kernelspec": {
261 | "display_name": "pybela-2uXYSGIe",
262 | "language": "python",
263 | "name": "python3"
264 | },
265 | "language_info": {
266 | "codemirror_mode": {
267 | "name": "ipython",
268 | "version": 3
269 | },
270 | "file_extension": ".py",
271 | "mimetype": "text/x-python",
272 | "name": "python",
273 | "nbconvert_exporter": "python",
274 | "pygments_lexer": "ipython3",
275 | "version": "3.9.19"
276 | },
277 | "orig_nbformat": 4
278 | },
279 | "nbformat": 4,
280 | "nbformat_minor": 2
281 | }
282 |
--------------------------------------------------------------------------------
/tutorials/notebooks/5_Logger.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# pybela Tutorial 5: Logger\n",
8 | "This notebook is a tutorial for the Logger class in the pybela python library. As opposed to the Streamer, the Logger stores variable values directly in binary files in the Bela board. This is more reliable than streaming data with the Streamer with the saving mode enabled, which depends on the websocket connection. The Logger will store the data in Bela even if the websocket connection is lost, and you can retrieve the data later. \n",
9 | "\n",
10 | "The complete documentation for the pybela library can be found in [https://belaplatform.github.io/pybela/](https://belaplatform.github.io/pybela/).\n",
11 | "\n",
12 | "As with the previous tutorials, you will need to run the `potentiometers` project in Bela. If you haven't done it yet, copy the project onto Bela:"
13 | ]
14 | },
15 | {
16 | "cell_type": "code",
17 | "execution_count": null,
18 | "metadata": {},
19 | "outputs": [],
20 | "source": [
21 | "!rsync -rvL ../bela-code/potentiometers root@bela.local:Bela/projects"
22 | ]
23 | },
24 | {
25 | "cell_type": "markdown",
26 | "metadata": {},
27 | "source": [
28 | "And compile and run the project using either the IDE or by running the following command in the Terminal:\n",
29 | "```bash\n",
30 | "ssh root@bela.local \"make -C Bela stop Bela PROJECT=potentiometers run\" \n",
31 | "```\n",
32 | "(Running this on a jupyter notebook will block the cell until the program is stopped on Bela.)"
33 | ]
34 | },
35 | {
36 | "cell_type": "markdown",
37 | "metadata": {},
38 | "source": [
39 | "You will need to connect the two potentiometers to the Bela analog inputs 0 and 1 (a schematic can be found in the `1_Streamer.ipynb` tutorial). \n",
40 | "\n",
41 | "Run the cells below to connect to the Logger to Bela and start logging data:"
42 | ]
43 | },
44 | {
45 | "cell_type": "code",
46 | "execution_count": null,
47 | "metadata": {},
48 | "outputs": [],
49 | "source": [
50 | "import os\n",
51 | "import pandas as pd\n",
52 | "from pybela import Logger"
53 | ]
54 | },
55 | {
56 | "cell_type": "code",
57 | "execution_count": null,
58 | "metadata": {},
59 | "outputs": [],
60 | "source": [
61 | "logger = Logger()\n",
62 | "logger.connect()"
63 | ]
64 | },
65 | {
66 | "cell_type": "markdown",
67 | "metadata": {},
68 | "source": [
69 | "### Logging files with automatic transfer\n",
70 | "\n",
71 | "Similarly to the Streamer and the Monitor, we can start and stop a logging session with `start_logging()` and `stop_logging`, and use asyncio to time the logging session:"
72 | ]
73 | },
74 | {
75 | "cell_type": "code",
76 | "execution_count": null,
77 | "metadata": {},
78 | "outputs": [],
79 | "source": [
80 | "file_paths = logger.start_logging(\n",
81 | " variables=[\"pot1\", \"pot2\"])\n",
82 | "logger.wait(0.5)\n",
83 | "logger.stop_logging()"
84 | ]
85 | },
86 | {
87 | "cell_type": "markdown",
88 | "metadata": {},
89 | "source": [
90 | "### Loading the data from the binary file\n",
91 | "The Logger automatically transfers the files from Bela to the computer whilst the logging session is happening. This avoids long waiting times at the end of the session. \n",
92 | "\n",
93 | "`start_logging()` returns `file_paths`, a dictionary containing the paths in Bela of the files generated and the local paths (in this computer) to which the files are copied. You can use these paths to automate the processing of the data:"
94 | ]
95 | },
96 | {
97 | "cell_type": "code",
98 | "execution_count": null,
99 | "metadata": {},
100 | "outputs": [],
101 | "source": [
102 | "data = {}\n",
103 | "for var in [\"pot1\", \"pot2\"]:\n",
104 | " data[var] = logger.read_binary_file(\n",
105 | " file_path=file_paths[\"local_paths\"][var], timestamp_mode=logger.get_prop_of_var(var, \"timestamp_mode\"))"
106 | ]
107 | },
108 | {
109 | "cell_type": "markdown",
110 | "metadata": {},
111 | "source": [
112 | "You might notice that the last buffer of each variable have 0.0 values at the end. This is because the buffers send by Bela to the Logger have a fixed size, and the last buffer might not be completely filled, so the remaining values are filled with 0.0. "
113 | ]
114 | },
115 | {
116 | "cell_type": "code",
117 | "execution_count": null,
118 | "metadata": {},
119 | "outputs": [],
120 | "source": [
121 | "data[\"pot1\"][\"buffers\"][-1]"
122 | ]
123 | },
124 | {
125 | "cell_type": "markdown",
126 | "metadata": {},
127 | "source": [
128 | "You can also flatten the buffers using:"
129 | ]
130 | },
131 | {
132 | "cell_type": "code",
133 | "execution_count": null,
134 | "metadata": {},
135 | "outputs": [],
136 | "source": [
137 | "flatten_data = {}\n",
138 | "for var in [\"pot1\", \"pot2\"]:\n",
139 | " flatten_data[var] = {\"timestamps\": [], \"data\": []}\n",
140 | " for _buffer in data[var][\"buffers\"]:\n",
141 | " flatten_data[var][\"timestamps\"].extend([_buffer[\"ref_timestamp\"] + i for i in range(len(_buffer[\"data\"]))])\n",
142 | " flatten_data[var][\"data\"].extend(_buffer[\"data\"])\n",
143 | " \n",
144 | "df = pd.DataFrame(flatten_data[\"pot1\"])\n",
145 | "df.head()"
146 | ]
147 | },
148 | {
149 | "cell_type": "markdown",
150 | "metadata": {},
151 | "source": [
152 | "Whilst the Logger by default transfers the binary files to Bela, the files are not removed from Bela. You can remove the files from Bela with `delete_file_from_bela()`:"
153 | ]
154 | },
155 | {
156 | "cell_type": "code",
157 | "execution_count": null,
158 | "metadata": {},
159 | "outputs": [],
160 | "source": [
161 | "for file in file_paths[\"remote_paths\"].values():\n",
162 | " logger.delete_file_from_bela(file, verbose=True)"
163 | ]
164 | },
165 | {
166 | "cell_type": "markdown",
167 | "metadata": {},
168 | "source": [
169 | "### Scheduling logging sessions\n",
170 | "You can schedule a logging session to start and stop at a specific time using the `schedule_logging()` method. This method takes the same arguments as `start_logging()`, but it also takes a `timestamps` and `durations` argument."
171 | ]
172 | },
173 | {
174 | "cell_type": "code",
175 | "execution_count": null,
176 | "metadata": {},
177 | "outputs": [],
178 | "source": [
179 | "latest_timestamp = logger.get_latest_timestamp() # get the latest timestamp\n",
180 | "sample_rate = logger.sample_rate # get the sample rate\n",
181 | "start_timestamp = latest_timestamp + sample_rate # start logging 1 second after the latest timestamp\n",
182 | "duration = sample_rate * 2 # log for 2 seconds\n",
183 | "\n",
184 | "file_paths = logger.schedule_logging(\n",
185 | " variables=[\"pot1\", \"pot2\"],\n",
186 | " timestamps=[start_timestamp, start_timestamp],\n",
187 | " durations=[duration, duration], \n",
188 | " transfer=True, \n",
189 | " logging_dir=\"./\")\n"
190 | ]
191 | },
192 | {
193 | "cell_type": "markdown",
194 | "metadata": {},
195 | "source": [
196 | "### Logging files without automatic transfer\n",
197 | "\n",
198 | "Alternatively, you can set `transfer=False` in `start_logging()` and transfer the files manually with `copy_file_from_bela()`:"
199 | ]
200 | },
201 | {
202 | "cell_type": "code",
203 | "execution_count": null,
204 | "metadata": {},
205 | "outputs": [],
206 | "source": [
207 | "file_paths = logger.start_logging(\n",
208 | " variables= [\"pot1\", \"pot2\"], transfer=False)\n",
209 | "logger.wait(0.5)\n",
210 | "logger.stop_logging()\n",
211 | "\n",
212 | "file_paths"
213 | ]
214 | },
215 | {
216 | "cell_type": "markdown",
217 | "metadata": {},
218 | "source": [
219 | "Note that the dict in `local_paths` is empty: since we disabled the automatic transfer, no local paths have been assigned. We can now copy the files using `copy_file_from_bela()`:"
220 | ]
221 | },
222 | {
223 | "cell_type": "code",
224 | "execution_count": null,
225 | "metadata": {},
226 | "outputs": [],
227 | "source": [
228 | "for var in [\"pot1\", \"pot2\"]:\n",
229 | " logger.copy_file_from_bela(remote_path=file_paths[\"remote_paths\"][var], local_path=os.path.basename(file_paths[\"remote_paths\"][var]))"
230 | ]
231 | },
232 | {
233 | "cell_type": "markdown",
234 | "metadata": {},
235 | "source": [
236 | "You might end up with a few `.bin` files in Bela. You can either remove them one by one with `delete_file_from_bela()` as explained above, or remove all `.bin` files in the Bela project with `delete_all_bin_files_in_project()``"
237 | ]
238 | },
239 | {
240 | "cell_type": "code",
241 | "execution_count": null,
242 | "metadata": {},
243 | "outputs": [],
244 | "source": [
245 | "logger.delete_all_bin_files_in_project()"
246 | ]
247 | }
248 | ],
249 | "metadata": {
250 | "kernelspec": {
251 | "display_name": "pybela-2uXYSGIe",
252 | "language": "python",
253 | "name": "python3"
254 | },
255 | "language_info": {
256 | "codemirror_mode": {
257 | "name": "ipython",
258 | "version": 3
259 | },
260 | "file_extension": ".py",
261 | "mimetype": "text/x-python",
262 | "name": "python",
263 | "nbconvert_exporter": "python",
264 | "pygments_lexer": "ipython3",
265 | "version": "3.9.19"
266 | },
267 | "orig_nbformat": 4
268 | },
269 | "nbformat": 4,
270 | "nbformat_minor": 2
271 | }
272 |
--------------------------------------------------------------------------------
/tutorials/notebooks/6_Controller.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# pybela Tutorial 6: Controller\n",
8 | "This notebook is a tutorial for the Controller class in the pybela python library. The Controller class allows you to control the variables in the Bela program using python. \n",
9 | "\n",
10 | "The Controller class has some limitations: you can only send one value at a time (no buffers) and you can not control the exact frame at which the values will be updated in the Bela program. Moreover, you can't use it at the same time as the Monitor. However, it is still a useful tool if you want to modify variable values in the Bela program without caring too much about the rate and exact timing of the updates.\n",
11 | "\n",
12 | "The complete documentation for the pybela library can be found in [https://belaplatform.github.io/pybela/](https://belaplatform.github.io/pybela/).\n",
13 | "\n",
14 | "As with the previous tutorials, you will need to run the `potentiometers` project in Bela. If you haven't done it yet, copy the project onto Bela:"
15 | ]
16 | },
17 | {
18 | "cell_type": "code",
19 | "execution_count": null,
20 | "metadata": {},
21 | "outputs": [],
22 | "source": [
23 | "!rsync -rvL ../bela-code/potentiometers root@bela.local:Bela/projects"
24 | ]
25 | },
26 | {
27 | "cell_type": "markdown",
28 | "metadata": {},
29 | "source": [
30 | "And compile and run the project using either the IDE or by running the following command in the Terminal:\n",
31 | "```bash\n",
32 | "ssh root@bela.local \"make -C Bela stop Bela PROJECT=potentiometers run\" \n",
33 | "```\n",
34 | "(Running this on a jupyter notebook will block the cell until the program is stopped on Bela.)\n",
35 | "\n",
36 | "First, let's import the `Controller` class from the `pybela` library and create a `Controller` object. Remember to run `.connect()` every time you instantiate a `pybela` object to connect to the Bela program. "
37 | ]
38 | },
39 | {
40 | "cell_type": "code",
41 | "execution_count": null,
42 | "metadata": {},
43 | "outputs": [],
44 | "source": [
45 | "from pybela import Controller\n",
46 | "\n",
47 | "controller = Controller()\n",
48 | "controller.connect()"
49 | ]
50 | },
51 | {
52 | "cell_type": "markdown",
53 | "metadata": {},
54 | "source": [
55 | "Run `.start_controlling()` to start controlling the variables in the Bela program."
56 | ]
57 | },
58 | {
59 | "cell_type": "code",
60 | "execution_count": null,
61 | "metadata": {},
62 | "outputs": [],
63 | "source": [
64 | "controller.start_controlling(variables=['pot1', 'pot2'])"
65 | ]
66 | },
67 | {
68 | "cell_type": "markdown",
69 | "metadata": {},
70 | "source": [
71 | "We can check which variables are being controlled with the `.get_controlled_status()` method."
72 | ]
73 | },
74 | {
75 | "cell_type": "code",
76 | "execution_count": null,
77 | "metadata": {},
78 | "outputs": [],
79 | "source": [
80 | "controller.get_controlled_status()"
81 | ]
82 | },
83 | {
84 | "cell_type": "markdown",
85 | "metadata": {},
86 | "source": [
87 | "Let's check what their current value is:"
88 | ]
89 | },
90 | {
91 | "cell_type": "code",
92 | "execution_count": null,
93 | "metadata": {},
94 | "outputs": [],
95 | "source": [
96 | "controller.get_value(variables=['pot1', 'pot2'])"
97 | ]
98 | },
99 | {
100 | "cell_type": "markdown",
101 | "metadata": {},
102 | "source": [
103 | "Let's now send a value to `pot1` and `pot2` using the `.send_value()` method."
104 | ]
105 | },
106 | {
107 | "cell_type": "code",
108 | "execution_count": null,
109 | "metadata": {},
110 | "outputs": [],
111 | "source": [
112 | "controller.send_value(variables=['pot1', 'pot2'], values=[0.5, 0.5])"
113 | ]
114 | },
115 | {
116 | "cell_type": "markdown",
117 | "metadata": {},
118 | "source": [
119 | "We can check if the variable values have been updated:"
120 | ]
121 | },
122 | {
123 | "cell_type": "code",
124 | "execution_count": null,
125 | "metadata": {},
126 | "outputs": [],
127 | "source": [
128 | "controller.get_value(variables=['pot1', 'pot2'])"
129 | ]
130 | },
131 | {
132 | "cell_type": "markdown",
133 | "metadata": {},
134 | "source": [
135 | "The controlled value will stay so until we send a new value or stop controlling the variable. We can stop controlling the variables with the `.stop_controlling()` method."
136 | ]
137 | },
138 | {
139 | "cell_type": "code",
140 | "execution_count": null,
141 | "metadata": {},
142 | "outputs": [],
143 | "source": [
144 | "controller.stop_controlling(variables=['pot1', 'pot2'])\n",
145 | "controller.get_value(variables=['pot1', 'pot2']) "
146 | ]
147 | },
148 | {
149 | "cell_type": "markdown",
150 | "metadata": {},
151 | "source": [
152 | "You should note that the values modified with the Controller class will only be visible through the Controller `get_value())` method and not through the Monitor, Streamer or Logger. The values in the Bela program will be updated with the values sent by the Controller, but the Monitor, Streamer or Logger will instead send the value of the variable in the Bela program if it hadn't been modified by the Controller. The reason behind this is that the Controller class has a different use case than the Monitor, Streamer or Logger (controlling variables in the code vs. collecting data), and it is not meant to be used at the same time as the other classes."
153 | ]
154 | }
155 | ],
156 | "metadata": {
157 | "kernelspec": {
158 | "display_name": "pybela-2uXYSGIe",
159 | "language": "python",
160 | "name": "python3"
161 | },
162 | "language_info": {
163 | "codemirror_mode": {
164 | "name": "ipython",
165 | "version": 3
166 | },
167 | "file_extension": ".py",
168 | "mimetype": "text/x-python",
169 | "name": "python",
170 | "nbconvert_exporter": "python",
171 | "pygments_lexer": "ipython3",
172 | "version": "3.9.19"
173 | }
174 | },
175 | "nbformat": 4,
176 | "nbformat_minor": 2
177 | }
178 |
--------------------------------------------------------------------------------
/tutorials/notebooks/7_Sparse-timestamping.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# pybela Tutorial 7: Sparse timestamping\n",
8 | "In the potentiometer example used in the previous tutorials, the values for `pot1` and `pot2` are assigned at every audio frame. Let's take a look again at the `render()` loop (the Bela code for this example can be found in (in `bela-code/potentiometers/render.cpp`).\n",
9 | "\n",
10 | "```cpp\n",
11 | "void render(BelaContext *context, void *userData)\n",
12 | "{\n",
13 | "\tfor(unsigned int n = 0; n < context->audioFrames; n++) {\n",
14 | "\t\tif(gAudioFramesPerAnalogFrame && !(n % gAudioFramesPerAnalogFrame)) {\n",
15 | "\t\t\t\n",
16 | "\t\t\tuint64_t frames = context->audioFramesElapsed/gAudioFramesPerAnalogFrame + n/gAudioFramesPerAnalogFrame;\n",
17 | "\t\t\tBela_getDefaultWatcherManager()->tick(frames); // watcher timestamps\n",
18 | "\t\t\t\n",
19 | "\t\t\tpot1 = analogRead(context, n/gAudioFramesPerAnalogFrame, gPot1Ch);\n",
20 | "\t\t\tpot2 = analogRead(context, n/gAudioFramesPerAnalogFrame, gPot2Ch);\n",
21 | "\t\t\t\n",
22 | "\t\t}\n",
23 | "\t}\n",
24 | "}\n",
25 | "```\n",
26 | "\n",
27 | "\n",
28 | "The Watched clock is also \"ticked\" at every analog frame, so that the timestamps in the data correspond to the audio frames in the Bela code. The data buffers we received from Bela in the Streamer and the Logger had this form: `{\"ref_timestamp\": 92381, \"data\":[0.34, 0.45, ...]}`. Each data point is registered in the buffer every time we assign a value to `pot1` and `pot2` in the Bela code. The `ref_timestamp` corresponds to the timestamp of the first sample in the `data` array, in this case `0.34`. Since in the Bela code, we assign `pot1` and `pot2` at every audio frame, we can infer the timestamps of each value in the data array by incrementing `ref_timestamp` by 1 for each sample. \n",
29 | "\n",
30 | "This is an efficient way of storing data since instead of storing the timestamp of every item in the data array, we only store the timestamp of the first item. We call this *dense* timestamping. However, for many applications, we might not assign a value to a variable every frame, we might do it more than once per frame, once every few frames, or we might want to do it at irregular intervals. In these cases, we need to store the timestamp of every item in the data array. We call this *sparse* timestamping.\n",
31 | "\n",
32 | "In this tutorial we take a look at *sparse* timestamping. The complete documentation for the pybela library can be found in [https://belaplatform.github.io/pybela/](https://belaplatform.github.io/pybela/).\n",
33 | "\n",
34 | "First, transfer the Bela code we will use in this tutorial to Bela:\n"
35 | ]
36 | },
37 | {
38 | "cell_type": "code",
39 | "execution_count": null,
40 | "metadata": {},
41 | "outputs": [],
42 | "source": [
43 | "!rsync -rvL ../bela-code/timestamping root@bela.local:Bela/projects"
44 | ]
45 | },
46 | {
47 | "cell_type": "markdown",
48 | "metadata": {},
49 | "source": [
50 | "Then you can compile and run the project using either the IDE or by running the following command in the Terminal:\n",
51 | "```bash\n",
52 | "ssh root@bela.local \"make -C Bela stop Bela PROJECT=potentiometers run\" \n",
53 | "```\n",
54 | "(Running this on a jupyter notebook will block the cell until the program is stopped on Bela.)"
55 | ]
56 | },
57 | {
58 | "cell_type": "markdown",
59 | "metadata": {},
60 | "source": [
61 | "As in the previous tutorials, we will use two potentiometers connected to Bela analog inputs 0 and 1. Check the `1_Streamer.ipnyb` tutorial notebook for instructions on how to set up the circuit. \n",
62 | "\n",
63 | "### Bela C++ code\n",
64 | "\n",
65 | "\n",
66 | "First, let's take a look at the Bela code. First, we have added `WatcherManager::kTimestampSample` to the declaration of `pot2`. This informs the Bela Watcher that `pot2` will be watched sparsely, that is, that the watcher will store a timestamp for every value assigned to `pot2`:\n",
67 | "\n",
68 | "```cpp\n",
69 | "Watcher pot1(\"pot1\");\n",
70 | "Watcher pot2(\"pot2\", WatcherManager::kTimestampSample);\n",
71 | "```\n",
72 | "\n",
73 | "Now let's take a look at `render()`:\n",
74 | "\n",
75 | "```cpp\n",
76 | "void render(BelaContext *context, void *userData)\n",
77 | "{\n",
78 | "\tfor(unsigned int n = 0; n < context->audioFrames; n++) {\n",
79 | "\t\tif(gAudioFramesPerAnalogFrame && !(n % gAudioFramesPerAnalogFrame)) {\n",
80 | "\t\t\t\n",
81 | "\t\t\tuint64_t frames = context->audioFramesElapsed/gAudioFramesPerAnalogFrame + n/gAudioFramesPerAnalogFrame;\n",
82 | "\t\t\tBela_getDefaultWatcherManager()->tick(frames); // watcher timestamps\n",
83 | "\t\t\t\n",
84 | "\t\t\tpot1 = analogRead(context, n/gAudioFramesPerAnalogFrame, gPot1Ch);\n",
85 | "\n",
86 | "\t\t\tif (frames % 12==0){\n",
87 | "\t\t\t\tpot2 = analogRead(context, n/gAudioFramesPerAnalogFrame, gPot2Ch);\n",
88 | "\t\t\t}\n",
89 | "\t\t}\n",
90 | "\t}\n",
91 | "}\n",
92 | "```\n",
93 | "\n",
94 | "We are \"ticking\" the Bela Watcher once per analog frame, so that the timestamps in the data correspond to the analog frames in the Bela code. We are assigning a value to `pot1` at every analog frame, as in the previous examples, but we are now only assigning a value to `pot2` every 12 frames. \n",
95 | "\n",
96 | "### Dealing with sparse timestamps in Python\n",
97 | "\n",
98 | "Let's now take a look at the data we receive from Bela. We will use the Streamer. Run the cells below to declare and connect the Streamer to Bela:"
99 | ]
100 | },
101 | {
102 | "cell_type": "code",
103 | "execution_count": null,
104 | "metadata": {},
105 | "outputs": [],
106 | "source": [
107 | "import pandas as pd\n",
108 | "from pybela import Streamer"
109 | ]
110 | },
111 | {
112 | "cell_type": "code",
113 | "execution_count": null,
114 | "metadata": {},
115 | "outputs": [],
116 | "source": [
117 | "streamer = Streamer()\n",
118 | "streamer.connect()"
119 | ]
120 | },
121 | {
122 | "cell_type": "markdown",
123 | "metadata": {},
124 | "source": [
125 | "We can call `.list()` to take a look at the variables available to be streamed, their types and timestamp mode:"
126 | ]
127 | },
128 | {
129 | "cell_type": "code",
130 | "execution_count": null,
131 | "metadata": {},
132 | "outputs": [],
133 | "source": [
134 | "streamer.list()"
135 | ]
136 | },
137 | {
138 | "cell_type": "markdown",
139 | "metadata": {},
140 | "source": [
141 | "`timestampMode` indicates if the timestamping is *sparse* (1) or *dense* (0). Now let's stream the data from Bela. We will stream `pot1` and `pot2`:"
142 | ]
143 | },
144 | {
145 | "cell_type": "code",
146 | "execution_count": null,
147 | "metadata": {},
148 | "outputs": [],
149 | "source": [
150 | "streamer.start_streaming(variables=[\"pot1\", \"pot2\"], saving_enabled=False)\n",
151 | "streamer.wait(2)\n",
152 | "streamer.stop_streaming()"
153 | ]
154 | },
155 | {
156 | "cell_type": "markdown",
157 | "metadata": {},
158 | "source": [
159 | "Now let's take a look at the streamed buffers for \"pot2\". Each buffer has the form `{\"ref_timestamp\": 912831, \"data\":[0.23, 0.24, ...], \"rel_timestamps\":[ 0, 12, ...]}`. `ref_timestamp` corresponds, as in the dense case, to the timestamp of the first data point in the `data` array. `rel_timestamps` is an array of timestamps relative to `ref_timestamp`. In this case, since we are assigning a value to `pot2` every 12 frames, the timestamps in `rel_timestamps` are `[0, 12, 24, 36, etc.]`."
160 | ]
161 | },
162 | {
163 | "cell_type": "code",
164 | "execution_count": null,
165 | "metadata": {},
166 | "outputs": [],
167 | "source": [
168 | "streamer.streaming_buffers_queue[\"pot2\"]"
169 | ]
170 | },
171 | {
172 | "cell_type": "markdown",
173 | "metadata": {},
174 | "source": [
175 | "You can now calculate the absolute timestamps of each data point by adding the values in `rel_timestamps` to `ref_timestamp`:"
176 | ]
177 | },
178 | {
179 | "cell_type": "code",
180 | "execution_count": null,
181 | "metadata": {},
182 | "outputs": [],
183 | "source": [
184 | "[streamer.streaming_buffers_queue[\"pot2\"][0][\"ref_timestamp\"]]*len(streamer.streaming_buffers_queue[\"pot2\"][0][\"rel_timestamps\"]) + streamer.streaming_buffers_queue[\"pot2\"][0][\"rel_timestamps\"]"
185 | ]
186 | },
187 | {
188 | "cell_type": "code",
189 | "execution_count": null,
190 | "metadata": {},
191 | "outputs": [],
192 | "source": [
193 | "pot2_data = {\"timestamps\":[], \"data\":[]}\n",
194 | "\n",
195 | "for _buffer in streamer.streaming_buffers_queue[\"pot2\"]:\n",
196 | " pot2_data[\"timestamps\"].extend([_buffer[\"ref_timestamp\"] + i for i in _buffer[\"rel_timestamps\"]])\n",
197 | " pot2_data[\"data\"].extend(_buffer[\"data\"])"
198 | ]
199 | },
200 | {
201 | "cell_type": "markdown",
202 | "metadata": {},
203 | "source": [
204 | "Note that the timestamps are spaced by 12, as expected:"
205 | ]
206 | },
207 | {
208 | "cell_type": "code",
209 | "execution_count": null,
210 | "metadata": {},
211 | "outputs": [],
212 | "source": [
213 | "df = pd.DataFrame(pot2_data)\n",
214 | "df.head()"
215 | ]
216 | }
217 | ],
218 | "metadata": {
219 | "kernelspec": {
220 | "display_name": "pybela-2uXYSGIe",
221 | "language": "python",
222 | "name": "python3"
223 | },
224 | "language_info": {
225 | "codemirror_mode": {
226 | "name": "ipython",
227 | "version": 3
228 | },
229 | "file_extension": ".py",
230 | "mimetype": "text/x-python",
231 | "name": "python",
232 | "nbconvert_exporter": "python",
233 | "pygments_lexer": "ipython3",
234 | "version": "3.9.19"
235 | },
236 | "orig_nbformat": 4
237 | },
238 | "nbformat": 4,
239 | "nbformat_minor": 2
240 | }
241 |
--------------------------------------------------------------------------------
/tutorials/notebooks/potentiometers-circuit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BelaPlatform/pybela/bb6332fa5b9968d14f2b58a55f130ee2dd09d1ef/tutorials/notebooks/potentiometers-circuit.png
--------------------------------------------------------------------------------