Mainly following the instructions here: Jetson AI Lab initial setup and NVIDIA Jetson Orin Nano getting started. Steps:
- Formatting the MicroSD
- Connect MicroSD to computer
- Format the MicroSD: download, install, run
- https://www.sdcard.org/downloads/formatter/sd-memory-card-formatter-for-windows-download/
- Select the MicroSD card. Use the quick format option. Leave volume label blank. Format.
⚠️ Formatting the MicroSD will wipe it in the process.
- Flashing the MicroSD with JetPack 6.2.1
Our Jetson Orin Nanos already have the updated firmware (36.+); therefore we do not need to update the firmware.
- Download the JetPack 6.2.1 image: JetPack 6.2.1 SDK
- Choose the "For Jetson Orin Nano Developer Kit currently running JetPack 6.x" option and download the MicroSD card image
As we have the updated firmware, we can use this image right off the bat.
- Download, install, and run balenaEtcher: balenaEtcher
- Choose "Flash from file" and select the downloaded JetPack 6.2.1 zip file
- Select the MicroSD as the target and then hit "Flash!"
This will take some time — 30 minutes at least, likely more.
- Booting Up the Jetson with the Flashed Image
- Insert the MicroSD card into the Jetson Orin Nano's MicroSD card slot (under the board with the fan, on the outside edge)
- Connect a mouse and keyboard + monitor
- Plug in the power cable
- Should boot up with the image!
Notes:
- You will likely have to do a couple of reboot cycles, especially once you connect to the internet as the automatic updates install. If boot fails:
- Use the boot menu to select the MicroSD as the default boot device.
- From the boot menu, you can also confirm the firmware version is 36.+ if needed.
- To set up the Tapo C120 for use with the Jetson Orin Nano, follow the instructions in the box and install the TP-Link Tapo app.
- Create a Tapo account and follow the on-screen steps to add the camera.
- After the camera is added, open it in the app, tap the settings icon (top-right), then go to "Advanced Settings" → "Camera Account" and create a camera account (this is different from your app account).
- Find your camera's IP address in "Advanced Settings" → "Network Settings". While you're there, turn on "Static IP".
Simple Python OpenCV script (run this after the Python Virtual Environment Setup)
import cv2
# Camera account credentials (set these to your camera account)
user = "<camera-account-username>"
password = "<camera-account-password>"
camera_ip = "<camera-ip-address>"
# Choose stream: /stream1 (higher quality) or /stream2 (lower latency/bitrate)
rtsp_url = f"rtsp://{user}:{password}@{camera_ip}/stream2"
print(f"Attempting to reach {rtsp_url}")
cap = cv2.VideoCapture(rtsp_url)
if not cap.isOpened():
print("Unable access the camera")
print("Check crendentials/IP and confirm the Jetson is on the same network as the camera.")
else:
print("Camera accessed")
while cap.isOpened:
ret, frame = cap.read()
if ret:
cv2.imshow("Captured Frame", frame)
else:
print("Error: Could not capture frame.")
break
if cv2.waitKey(1) & 0xFF == ord("q"):
break
cap.release()
cv2.destroyAllWindows()Ensure both the camera and the Jetson Orin Nano are on the same network; otherwise the Jetson will not be able to access the camera.
We have an Adafruit Feather nRF52840 microcontroller which includes sensors for air temperature, humidity, and light level. To connect to this to the Jetson, simply use a USB to USB-C connector. The Adafruit ENS160 MOX Gas Sensor is also connected to the microcontroller through pins. In the future, these pins and the pinholes on the microcontroller should be soldered together, but currently, follow this setup when connecting the two.
- Board 3V to sensor VIN (red wire)
- Board GND to sensor GND (black wire)
- Board SCL to sensor SCL (yellow wire)
- Board SDA to sensor SDA (blue wire)
In the ardino environment, save this code with the name adafruit_with_ens_start_stop_final:
Note: If you do not have the correct board selected in Arduino to run this code, you may have to follow the following steps:
- Go to File -> Preferences -> Additional Boards Manager URLs: and add this URL https://adafruit.github.io/arduino-board-index/package_adafruit_index.json
- Then go to Tools -> Board -> Boards Manager and install the Adafruit nRF52 boards libarary
- Run the program on the Adafruit Feather nRF52840 Sense
You will also need to add your own library to allow the microcontroller to recognize start and stop words.
- Download this .zip file https://github.com/crcresearch/Astra/blob/main/ei-speech-recognition-correct-words-arduino-1.0.1.zip
- Go to Sketch -> Include library -> Add .Zip library and navigate to where the downloaded .zip file is
Finally, you may need to add any of the neccessary libararies included in the header files at the top of the code. To do so, go to Tools -> Manage Libaraies.
/* MODIFIED VERSION
* Edge Impulse ingestion SDK
* Copyright (c) 2022 EdgeImpulse Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
//Use "VOICE START" and "VOICE STOP" to start and stop this program. Includes all sensors including gas.
namespace std {
void __throw_out_of_range_fmt(const char* fmt, ...) {
Serial.println("ERROR: Vector out of bounds access detected!");
Serial.println("Edge Impulse library tried to access invalid vector index");
Serial.flush(); // Make sure the message gets sent before freezing
while(1) {
delay(1000); // Stop execution
}
}
}
static const int PDM_DATA_PIN = 34;
static const int PDM_CLOCK_PIN = 35;
static const int PDM_POWER_PIN = -1; // no power-enable pin
// If your target is limited in memory remove this macro to save 10K RAM
#define EIDSP_QUANTIZE_FILTERBANK 0
/**
* Define the number of slices per model window. E.g. a model window of 1000 ms
* with slices per model window set to 4. Results in a slice size of 250 ms.
* For more info: https://docs.edgeimpulse.com/docs/continuous-audio-sampling
*/
#define EI_CLASSIFIER_SLICES_PER_MODEL_WINDOW 2
/*
** NOTE: If you run into TFLite arena allocation issue.
**
** This may be due to may dynamic memory fragmentation.
** Try defining "-DEI_CLASSIFIER_ALLOCATION_STATIC" in boards.local.txt (create
** if it doesn't exist) and copy this file to
** `<ARDUINO_CORE_INSTALL_PATH>/arduino/hardware/<mbed_core>/<core_version>/`.
**
** See
** (https://support.arduino.cc/hc/en-us/articles/360012076960-Where-are-the-installed-cores-located-)
** to find where Arduino installs cores on your machine.
**
** If the problem persists then there's not enough memory for this model and application.
*/
/* Includes ---------------------------------------------------------------- */
#include <PDM.h>
#include <speech-recognition-correct-words_inferencing.h>
#include <Adafruit_APDS9960.h>
#include <Adafruit_BMP280.h>
#include <Adafruit_LIS3MDL.h>
#include <Adafruit_LSM6DS33.h>
#include <Adafruit_LSM6DS3TRC.h>
#include <Adafruit_SHT31.h>
#include <Adafruit_Sensor.h>
#include <Wire.h>
#include "ScioSense_ENS160.h" // ENS160 library
// Define variables
Adafruit_APDS9960 apds9960; // proximity, light, color, gesture
Adafruit_BMP280 bmp280; // temperautre, barometric pressure
Adafruit_LIS3MDL lis3mdl; // magnetometer
Adafruit_LSM6DS3TRC lsm6ds3trc; // accelerometer, gyroscope
Adafruit_LSM6DS33 lsm6ds33;
Adafruit_SHT31 sht30; // humidity
ScioSense_ENS160 ens160(ENS160_I2CADDR_1); // air quality sensor
bool was_recording = false;
uint8_t proximity;
uint16_t r, g, b, c;
float temperature, pressure, altitude;
float magnetic_x, magnetic_y, magnetic_z;
float accel_x, accel_y, accel_z;
float gyro_x, gyro_y, gyro_z;
float humidity;
int32_t mic;
uint16_t aqi, tvoc, eco2; // ENS160 readings
long int accel_array[6];
long int check_array[6]={0.00, 0.00, 0.00, 0.00, 0.00, 0.00};
bool is_recording = false;
int sensor_counter;
bool ens160_available = false; // Track ENS160 status
bool new_rev = true;
/** Audio buffers, pointers and selectors */
typedef struct {
signed short *buffers[2];
unsigned char buf_select;
unsigned char buf_ready;
unsigned int buf_count;
unsigned int n_samples;
} inference_t;
static inference_t inference;
static bool record_ready = false;
static signed short *sampleBuffer;
static bool debug_nn = false; // Set this to true to see e.g. features generated from the raw signal
static int print_results = -(EI_CLASSIFIER_SLICES_PER_MODEL_WINDOW);
static const int led_pin = LED_BUILTIN;
/**
* @brief Arduino setup function
*/
void setup()
{
pinMode(led_pin, OUTPUT);
// put your setup code here, to run once:
Serial.begin(115200);
// comment out the below line to cancel the wait for USB connection (needed for native USB)
while (!Serial);
Serial.println("Edge Impulse + ENS160 Inferencing Demo");
// initialize the sensors
apds9960.begin();
apds9960.enableProximity(true);
apds9960.enableColor(true);
bmp280.begin();
lis3mdl.begin_I2C();
lsm6ds33.begin_I2C();
// check for readings from LSM6DS33
sensors_event_t accel;
sensors_event_t gyro;
sensors_event_t temp;
lsm6ds33.getEvent(&accel, &gyro, &temp);
accel_array[0] = accel.acceleration.x;
accel_array[1] = accel.acceleration.y;
accel_array[2] = accel.acceleration.z;
accel_array[3] = gyro.gyro.x;
accel_array[4] = gyro.gyro.y;
accel_array[5] = gyro.gyro.z;
// if all readings are empty, then new rev
for (int i =0; i < 5; i++) {
if (accel_array[i] != check_array[i]) {
new_rev = false;
break;
}
}
// and we need to instantiate the LSM6DS3TRC
if (new_rev) {
lsm6ds3trc.begin_I2C();
}
sht30.begin();
// Initialize ENS160
Serial.print("ENS160...");
bool ens160_ok = ens160.begin();
ens160_available = ens160.available();
Serial.println(ens160_available ? "done." : "failed!");
if (ens160_available) {
// Print ENS160 versions
Serial.print("\tRev: ");
Serial.print(ens160.getMajorRev());
Serial.print(".");
Serial.print(ens160.getMinorRev());
Serial.print(".");
Serial.println(ens160.getBuild());
// Set to standard operating mode
Serial.print("\tStandard mode ");
Serial.println(ens160.setMode(ENS160_OPMODE_STD) ? "done." : "failed!");
// Set initial environmental data
ens160.set_envdata(25.0, 50.0);
Serial.println("ENS160 warming up (30 seconds)...");
delay(30000); // 30 second warm-up for ENS160
Serial.println("ENS160 ready!");
} else {
Serial.println("ENS160 initialization failed - continuing without air quality data");
}
// tell the PDM driver which pins to use
PDM.setPins(PDM_DATA_PIN, PDM_CLOCK_PIN, PDM_POWER_PIN);
PDM.setBufferSize(512); // Smaller buffer
PDM.setGain(20); // Lower gain
if (!PDM.begin(1, 16000)) { // Lower sample rate
Serial.println("Failed to start PDM!");
while(1);
}
run_classifier_init();
if (microphone_inference_start(EI_CLASSIFIER_SLICE_SIZE) == false) {
ei_printf("ERR: Could not allocate audio buffer (size %d), this could be due to the window length of your model\r\n", EI_CLASSIFIER_RAW_SAMPLE_COUNT);
return;
}
Serial.println("System ready! Say your wake words to start/stop data transmission.");
}
/**
* @brief Arduino main function. Runs the inferencing loop.
*/
void loop()
{
bool m = microphone_inference_record();
if (!m) {
ei_printf("ERR: Failed to record audio...\n");
return;
}
signal_t signal;
signal.total_length = EI_CLASSIFIER_SLICE_SIZE;
signal.get_data = µphone_audio_signal_get_data;
ei_impulse_result_t result = {0};
EI_IMPULSE_ERROR r = run_classifier_continuous(&signal, &result, debug_nn);
if (r != EI_IMPULSE_OK) {
ei_printf("ERR: Failed to run classifier (%d)\n", r);
return;
}
// Start or stop sending data if value of word is above a threshold
bool should_record = is_recording;
if (result.classification[2].value > 0.7){
should_record = true;
//Serial.println("*** START command detected - Beginning sensor data transmission ***");
}
if (result.classification[3].value > 0.7){
should_record = false;
//Serial.println("*** STOP command detected - Ending sensor data transmission ***");
}
// Edge-detect: only print when the state changes
if (should_record && !was_recording){
is_recording = true;
was_recording = true;
Serial.println("START");
// CSV header, once per session:
Serial.println("HEADER,time_ms,red,green,blue,clear,temperatureC,humidityPct,accelZ_mps2,aqi,tvoc_ppb,eco2_ppm");
}
else if (!should_record && was_recording){
is_recording = false;
was_recording = false;
Serial.println("STOP");
}
if (is_recording) {
sensor_counter++;
if (sensor_counter >= 3) { // Only send every 3rd cycle
send_sensor_data();
sensor_counter = 0;
}
}
if (++print_results >= (EI_CLASSIFIER_SLICES_PER_MODEL_WINDOW)) {
// Optionally show classification confidence for debugging
/*
ei_printf("Predictions: ");
for (size_t ix = 0; ix < EI_CLASSIFIER_LABEL_COUNT; ix++) {
ei_printf("%s: %.2f ", result.classification[ix].label,
result.classification[ix].value);
}
ei_printf("\n");
*/
#if EI_CLASSIFIER_HAS_ANOMALY == 1
ei_printf(" anomaly score: %.3f\n", result.anomaly);
#endif
print_results = 0;
}
}
/**
* @brief PDM buffer full callback
* Get data and call audio thread callback
*/
static void pdm_data_ready_inference_callback(void)
{
int bytesAvailable = PDM.available();
// read into the sample buffer
int bytesRead = PDM.read((char *)&sampleBuffer[0], bytesAvailable);
if (record_ready == true) {
for (int i = 0; i<bytesRead>>1; i++) {
inference.buffers[inference.buf_select][inference.buf_count++] = sampleBuffer[i];
if (inference.buf_count >= inference.n_samples) {
inference.buf_select ^= 1;
inference.buf_count = 0;
inference.buf_ready = 1;
}
}
}
}
/**
* @brief Init inferencing struct and setup/start PDM
*
* @param[in] n_samples The n samples
*
* @return { description_of_the_return_value }
*/
static bool microphone_inference_start(uint32_t n_samples)
{
inference.buffers[0] = (signed short *)malloc(n_samples * sizeof(signed short));
if (inference.buffers[0] == NULL) {
return false;
}
inference.buffers[1] = (signed short *)malloc(n_samples * sizeof(signed short));
if (inference.buffers[1] == NULL) {
free(inference.buffers[0]);
return false;
}
sampleBuffer = (signed short *)malloc((n_samples >> 1) * sizeof(signed short));
if (sampleBuffer == NULL) {
free(inference.buffers[0]);
free(inference.buffers[1]);
return false;
}
inference.buf_select = 0;
inference.buf_count = 0;
inference.n_samples = n_samples;
inference.buf_ready = 0;
// configure the data receive callback
PDM.onReceive(&pdm_data_ready_inference_callback);
PDM.setBufferSize((n_samples >> 1) * sizeof(int16_t));
// initialize PDM with:
// - one channel (mono mode)
// - a 16 kHz sample rate
if (!PDM.begin(1, EI_CLASSIFIER_FREQUENCY)) {
ei_printf("Failed to start PDM!");
}
// set the gain, defaults to 20
PDM.setGain(127);
record_ready = true;
return true;
}
void onPDMdata() {
int bytesAvailable = PDM.available();
if (bytesAvailable) {
int bytesRead = PDM.read(sampleBuffer, bytesAvailable);
if (bytesRead != bytesAvailable) {
Serial.println("PDM read mismatch!");
}
}
}
/**
* @brief Wait on new data
*
* @return True when finished
*/
static bool microphone_inference_record(void)
{
bool ret = true;
if (inference.buf_ready == 1) {
ei_printf(
"Error sample buffer overrun. Decrease the number of slices per model window "
"(EI_CLASSIFIER_SLICES_PER_MODEL_WINDOW)\n");
ret = false;
}
while (inference.buf_ready == 0) {
delay(1);
}
inference.buf_ready = 0;
return ret;
}
/**
* Get raw audio signal data
*/
static int microphone_audio_signal_get_data(size_t offset, size_t length, float *out_ptr)
{
numpy::int16_to_float(&inference.buffers[inference.buf_select ^ 1][offset], out_ptr, length);
return 0;
}
/**
* @brief Stop PDM and release buffers
*/
static void microphone_inference_end(void)
{
PDM.end();
free(inference.buffers[0]);
free(inference.buffers[1]);
free(sampleBuffer);
}
static void send_sensor_data(){
// Read color sensor data
while (!apds9960.colorDataReady()) {
delay(5);
}
apds9960.getColorData(&r, &g, &b, &c);
// Read temperature and pressure
temperature = bmp280.readTemperature();
// Read magnetometer
lis3mdl.read();
// Read accelerometer/gyroscope
sensors_event_t accel;
sensors_event_t gyro;
sensors_event_t temp;
if (new_rev) {
lsm6ds3trc.getEvent(&accel, &gyro, &temp);
}
else {
lsm6ds33.getEvent(&accel, &gyro, &temp);
}
accel_z = accel.acceleration.z;
// Read humidity
humidity = sht30.readHumidity();
// Read ENS160 air quality data
if (ens160_available) {
// Update environmental data with real sensor readings
ens160.set_envdata(temperature, humidity);
// Perform measurement
ens160.measure();
// Get readings
aqi = ens160.getAQI();
tvoc = ens160.getTVOC();
eco2 = ens160.geteCO2();
} else {
// Set error values if sensor not available
aqi = 999;
tvoc = 65535;
eco2 = 65535;
}
// Print all sensor data
unsigned long t_ms = millis();
Serial.print("DATA,");
Serial.print(t_ms); Serial.print(',');
Serial.print(r); Serial.print(',');
Serial.print(g); Serial.print(',');
Serial.print(b); Serial.print(',');
Serial.print(c); Serial.print(',');
Serial.print(temperature, 2); Serial.print(',');
Serial.print(humidity, 2); Serial.print(',');
Serial.print(accel_z, 3); Serial.print(',');
Serial.print(aqi); Serial.print(',');
Serial.print(tvoc); Serial.print(',');
Serial.println(eco2);
delay(50);
}
#if !defined(EI_CLASSIFIER_SENSOR) || EI_CLASSIFIER_SENSOR != EI_CLASSIFIER_SENSOR_MICROPHONE
#error "Invalid model for current sensor."
#endifCreate a python file named process_data.py with this code:
# process_data.py
import sys, csv
out = csv.writer(open("sensor_log.csv", "w", newline=""))
out.writerow(["t_ms","r","g","b","c","temperatureC","humidityPct","accelZ_mps2","aqi","tvoc_ppb","eco2_ppm"])
print("process_data.py: READY", flush=True)
try:
for line in sys.stdin:
line = line.strip()
if not line:
continue
# ignore headers or chatter
if line.startswith("HEADER,"):
continue
if not line.startswith("DATA,"):
continue
fields = line.split(",", maxsplit=12)[1:] # everything after "DATA,"
# Expecting 11 fields based on our CSV
if len(fields) != 11:
continue
try:
t_ms = int(fields[0])
r = int(fields[1])
g = int(fields[2])
b = int(fields[3])
c = int(fields[4])
temperature = float(fields[5])
humidity = float(fields[6])
accel_z = float(fields[7])
aqi = int(fields[8])
tvoc = int(fields[9])
eco2 = int(fields[10])
except ValueError:
continue
# Write to CSV (or do your own processing instead)
out.writerow([t_ms,r,g,b,c,temperature,humidity,accel_z,aqi,tvoc,eco2])
# Example real-time print
print(f"T={t_ms}ms Temp={temperature:.2f}C RH={humidity:.1f}% eCO2={eco2}ppm TVOC={tvoc}ppb", flush=True)
except KeyboardInterrupt:
pass
print("process_data.py: EXIT", flush=True)Finally, in the same folder, create a python file called listener.py with this code:
# listener.py
import serial, subprocess, sys, signal
PORT = "/dev/ttyACM0" # Linux/Jetson example; Windows like "COM3"; macOS like "/dev/tty.usbmodemXXXX"
BAUD = 115200
proc = None # subprocess for process_data.py
def start_processing():
global proc
if proc is None or proc.poll() is not None:
# Launch process_data.py and open a text pipe for input
proc = subprocess.Popen(
[sys.executable, "process_data.py"],
stdin=subprocess.PIPE, text=True, bufsize=1
)
print("Started process_data.py")
def stop_processing():
global proc
if proc and proc.poll() is None:
print("Stopping process_data.py ...")
try:
# Close its stdin so it can exit gracefully
proc.stdin.close()
except Exception:
pass
# Politely ask it to stop
proc.send_signal(signal.SIGINT)
def main():
global proc
print(f"Opening serial {PORT} @ {BAUD} ...")
with serial.Serial(PORT, BAUD, timeout=1) as ser:
print(f"Listening on {PORT} ...")
while True:
raw = ser.readline()
if not raw:
continue
line = raw.decode(errors="ignore").strip()
if line == "START":
print("[SERIAL] START")
start_processing()
elif line == "STOP":
print("[SERIAL] STOP")
stop_processing()
proc = None
elif line.startswith("DATA,"):
# Forward DATA lines only if the processor is running
if proc and proc.poll() is None:
try:
proc.stdin.write(line + "\n")
proc.stdin.flush()
except BrokenPipeError:
# Child exited; drop data until next START
proc = None
# (Optionally print a dot or echo for debugging)
else:
# Unknown line; ignore or log
pass
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
stop_processing()
print("\nListener exiting.")Note that the PORT may be different depending on the device. To determine what port is correct, run these two lines in the terminal:
sudo dmesg | grep -i tty
ls /dev/ttyACM* /dev/ttyUSB* 2>/dev/nullThe port that these lines return is the port that you should copy and paste into the PORT variable of the listener.py script.
After this inital setup, you should be able to transmit sensor data from Arduino into Python in real time. To do so, follow these steps:
- Upload the adafruit_with_ens_start_stop_final code to the microcontroller.
- Note that to put the microcontroller into bootloader mode, you will need to press the rest button twice. The large light should go from red to green.
- DO NOT open the serial monitor.
- In the terminal, run
python3 listener.py- Note that the code takes 30 seconds to "warm up" due to the MOX gas sensor attached to the microcontroller.
- After 30 seconds, the wake word "VOICE, start" should begin data transmission.
- Everything that would be showing up in the Arduino serial monitor is now being transmitted to the Python code and should be showing up in the terminal.
- The phrase "VOICE, stop" should end data transmission.
The Snap package may not work properly on JetPack 6.x. Use Mozilla's PPA instead (friends don't let friends use Chrome). Remove the Snap installation and purge leftovers:
sudo snap remove firefox
sudo apt purge firefoxAdd the Mozilla Team PPA:
sudo add-apt-repository ppa:mozillateam/ppa
sudo apt updatePin the PPA so that APT will use it over the Snap:
echo '
Package: *
Pin: release o=LP-PPA-mozillateam
Pin-Priority: 1001
' | sudo tee /etc/apt/preferences.d/mozilla-firefoxInstall Firefox:
sudo apt install firefoxThis pulls the .deb directly from Mozilla's PPA instead of the broken/incompatible Snap.
Jetson Stats is a useful utility that shows system performance information such as GPU utilization.
To install it, run (outside of your virtual environment. Type deactivate to deactivate it):
sudo pip3 install -U jetson-statsYou may need to install pip for the system as JP6.2.1 comes with Python installed, but not pip:
sudo apt-get install python-pip python3-pipOnce installed, simply run:
jtopThe Jetson Orin Nano only has 8 GB of RAM. This is commonly not enough to run the LMs we wish to run or the work we wish to do. To "solve" that we set up more swap memory. Swap memory uses storage as memory when needed. As you can imagine, this is much slower than RAM, but still useful.
First, create these scripts: swap12g-on.sh
#!/bin/bash
set -e
echo "Disabling any active swap..."
sudo swapoff -a || true
echo "Creating 12G swapfile..."
sudo fallocate -l 12G /swapfile
echo "Setting permissions..."
sudo chmod 600 /swapfile
echo "Formatting swapfile..."
sudo mkswap /swapfile
echo "Enabling swap..."
sudo swapon /swapfile
echo "✅ Swap enabled:"
swapon --show
free -hswap12g-off.sh
#!/bin/bash
set -e
if [ -f /swapfile ]; then
echo "Disabling swap..."
sudo swapoff /swapfile
echo "Removing swapfile..."
sudo rm /swapfile
echo "✅ Swap removed."
else
echo "No /swapfile found — nothing to clean up."
fi
echo
swapon --show
free -hswap4g-on.sh
#!/bin/bash
set -e
echo "Disabling any active swap..."
sudo swapoff -a || true
echo "Creating 4G swapfile..."
sudo fallocate -l 4G /swapfile
echo "Setting permissions..."
sudo chmod 600 /swapfile
echo "Formatting swapfile..."
sudo mkswap /swapfile
echo "Enabling swap..."
sudo swapon /swapfile
echo "✅ Swap enabled:"
swapon --show
free -hMake them executable:
chmod +x swap12g-on.sh swap12g-off.sh swap4g-on.shEnable 12G swap memory:
./swap12g-on.shDisable swap memory:
./swap12g-off.shRestore default 4G of swap memory
./swap4g-on.shThe swap memory is 4G by default and the change to the swap memory is not permanent. A reboot will reset the swap memory configuration.
Simply run the installer:
curl -fsSL https://ollama.com/install.sh | sh# Pull
ollama pull gemma3n:e2b
# Run
ollama run gemma3n:e2b
⚠️ Ensure you have increased swap memory before running the gemma3n:e2b model.
Create a Python virtual environment:
python3 -m venv <venv-name-here>Activate it:
source <venv-name-here>/bin/activateThankfully, there are prebuilt torch and torchvision wheels with CUDA for the Jetson at Jetson AI Lab - cu126 index
We will be using these.
- Install needed system packages
- libopenblas-dev
sudo apt-get install -y libopenblas-dev- cuSPARSELt
Thankfully, NVIDIA provides an installer for this:
# Download the installer.
wget https://developer.download.nvidia.com/compute/cusparselt/0.8.1/local_installers/cusparselt-local-tegra-repo-ubuntu2204-0.8.1_0.8.1-1_arm64.deb
# Run the installer and then run the command it spits out.
sudo dpkg -i cusparselt-local-tegra-repo-ubuntu2204-0.8.1_0.8.1-1_arm64.deb
# This command is a template of what the above command spits out.
sudo cp /var/cusparselt-local-tegra-repo-ubuntu2204-0.8.1/cusparselt-*-keyring.gpg /usr/share/keyrings/
# Update
sudo apt-get update
# And install
sudo apt-get -y install cusparselt- cudss
Like cuSPARSELt, NVIDIA provides an installer for us:
# Download the installer.
wget https://developer.download.nvidia.com/compute/cudss/0.6.0/local_installers/cudss-local-tegra-repo-ubuntu2204-0.6.0_0.6.0-1_arm64.deb
# Run the installer and then run the command it spits out.
sudo dpkg -i cudss-local-tegra-repo-ubuntu2204-0.6.0_0.6.0-1_arm64.deb
# Template of what the above command will spit out.
sudo cp /var/cudss-local-tegra-repo-ubuntu2204-0.6.0/cudss-*-keyring.gpg /usr/share/keyrings/
sudo apt-get update
sudo apt-get -y install cudss- Install Torch and Torchvision
Ensure your Python virtual environment is active! We will be using the wheels made available to us from the Jetson AI Lab as building from source could take days. To do this, simply point pip to the Jetson AI Lab package index and install torch and torchvision:
pip install --force-reinstall --no-cache-dir -U torch torchvision --index-url https://pypi.jetson-ai-lab.io/jp6/cu126If for whatever reason that doesn't work, you can try downloading the wheels and then installing from those:
wget https://pypi.jetson-ai-lab.io/jp6/cu126/+f/590/92ab729aee2b8/torch-2.8.0-cp310-cp310-linux_aarch64.whl#sha256=59092ab729aee2b8937d80cc1b35d1128275bd02a7e1bc911e7efa375bd97226
wget https://pypi.jetson-ai-lab.io/jp6/cu126/+f/1c0/3de08a69e9554/torchvision-0.23.0-cp310-cp310-linux_aarch64.whl#sha256=1c03de08a69e95542024477e0cde95fab3436804917133d3f00e67629d3fe902
pip install ./<torch-wheel-file-here>
pip install ./<torchvision-wheel-file-here>NOTE: You may need to go to Jetson AI Lab and copy the up-to-date links for torch and torchvision.
Run these test scripts to ensure the GPU can be found and utilized by PyTorch:
Matrix Multiplication (CPU vs GPU timing)
import torch
import time
print("CUDA available:", torch.cuda.is_available())
print("GPU name:", torch.cuda.get_device_name(0) if torch.cuda.is_available() else "No GPU")
# Size of test matrices
size = 4000
a = torch.randn(size, size)
b = torch.randn(size, size)
# Run on CPU
start = time.time()
c = torch.matmul(a, b)
torch.cuda.synchronize() if torch.cuda.is_available() else None
print("CPU matmul time:", time.time() - start, "seconds")
# Run on GPU
if torch.cuda.is_available():
a_gpu = a.to("cuda")
b_gpu = b.to("cuda")
start = time.time()
c_gpu = torch.matmul(a_gpu, b_gpu)
torch.cuda.synchronize() # wait for GPU to finish
print("GPU matmul time:", time.time() - start, "seconds")Tiny CNN on MNIST
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
# Check GPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print("Using device:", device)
# Load MNIST
train_loader = torch.utils.data.DataLoader(
datasets.MNIST('.', train=True, download=True,
transform=transforms.ToTensor()),
batch_size=512, shuffle=True
)
# Define a simple CNN
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(1, 32, 3, 1)
self.fc1 = nn.Linear(5408, 128)
self.fc2 = nn.Linear(128, 10)
def forward(self, x):
x = F.relu(self.conv1(x))
x = F.max_pool2d(x, 2)
x = torch.flatten(x, 1)
x = F.relu(self.fc1(x))
x = self.fc2(x)
return F.log_softmax(x, dim=1)
model = Net().to(device)
optimizer = optim.Adam(model.parameters())
# Train 1 epoch (just to test GPU)
model.train()
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data.to(device), target.to(device)
optimizer.zero_grad()
output = model(data)
loss = F.nll_loss(output, target)
loss.backward()
optimizer.step()
if batch_idx % 50 == 0:
print(f"Train Step: {batch_idx}, Loss: {loss.item()}")
if batch_idx > 100: # stop early
breakEnsure that the GPU is found and used.
You can even see the GPU being utilized via
jtop!
Like we did for the PyTorch installation, we will be using prebuilt wheels as building from source could take days. Unfortunately, the Jetson AI Lab does not (at the time of this instructional write-up) have prebuilt wheels for TensorFlow. However, NVIDIA does (and PyTorch wheels as well, but not as updated as the Jetson AI Lab PyTorch wheel).
- Install needed system packages
sudo apt-get update
sudo apt-get install libhdf5-serial-dev hdf5-tools libhdf5-dev zlib1g-dev zip libjpeg8-dev liblapack-dev libblas-dev gfortran- Install TensorFlow
Ensure your Python virtual environment is active!
# Download the wheel
wget https://developer.download.nvidia.com/compute/redist/jp/v61/tensorflow/tensorflow-2.16.1+nv24.08-cp310-cp310-linux_aarch64.whl
# Install it
pip install ./tensorflow-2.16.1+nv24.08-cp310-cp310-linux_aarch64.whlIt's likely that you will see a versioning conflict with numpy; if so, do the following:
# Uninstall numpy
pip uninstall numpy
# Install compatible numpy version <2.x
# Numpy version 1.26.4 is the latest 1.x version
pip install numpy==1.26.4Run these test scripts to ensure the GPU can be found and utilized by TensorFlow:
Matrix Multiplication (CPU vs GPU timing)
import tensorflow as tf
import time
# Confirm GPU is detected
print("GPUs:", tf.config.list_physical_devices("GPU"))
# Create large random matrices
size = 4000
a = tf.random.normal([size, size])
b = tf.random.normal([size, size])
# Run on GPU
with tf.device("/GPU:0"):
start = time.time()
c = tf.matmul(a, b)
tf.experimental.numpy.copy(c) # force compute
end = time.time()
print("GPU matmul time:", end - start, "seconds")
# Run on CPU
with tf.device("/CPU:0"):
start = time.time()
c = tf.matmul(a, b)
tf.experimental.numpy.copy(c) # force compute
end = time.time()
print("CPU matmul time:", end - start, "seconds")Tiny CNN on MNIST
import tensorflow as tf
# Load dataset
(x_train, y_train), _ = tf.keras.datasets.mnist.load_data()
x_train = x_train.reshape(-1, 28, 28, 1).astype("float32") / 255.0
# Define a simple CNN
model = tf.keras.Sequential([
tf.keras.layers.Conv2D(32, 3, activation="relu", input_shape=(28,28,1)),
tf.keras.layers.MaxPooling2D(),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(128, activation="relu"),
tf.keras.layers.Dense(10, activation="softmax")
])
model.compile(optimizer="adam", loss="sparse_categorical_crossentropy", metrics=["accuracy"])
# Train briefly (just 1 epoch to check GPU usage)
model.fit(x_train, y_train, epochs=1, batch_size=512)Ensure that the GPU is found and used.
You can even see the GPU being utilized via
jtop!
This requires managing a few dependencies.
- Install Insightface:
pip install insightfaceThis installs various dependencies; remove opencv-python-headless:
pip uninstall -y opencv-python-headless- Install onnxruntime and onnx:
# Use the Jetson AI Lab's GPU build
pip install onnxruntime-gpu --index-url https://pypi.jetson-ai-lab.io/jp6/cu126
pip install onnx- Upgrade ml-dtypes: The version of ml-dtypes we are using (0.3.x) for TensorFlow is not compatible with onnxruntime. However, upgrading it doesn't seem to break TensorFlow, so:
pip install ml-dtypes==0.5.3We can make use of the Jetson AI Lab's pre-built wheel:
pip install opencv-contrib-python --no-deps --index-url https://pypi.jetson-ai-lab.io/jp6/cu126The --no-deps flag avoids changing your numpy version.
Confirm that GStreamer and v4l/v4l2 are enabled:
python -c "import cv2; print(cv2.getBuildInformation())"- Install needed system packages
sudo apt-get install libasound-dev portaudio-19-dev libportaudio2 libportaudiocpp0- Install PyAudio
pip install PyAudioThe remaining packages from the Requirements.txt can simply be pip installed:
pip install librosa scikit-learn scipy transformers safetensors sounddevice matplotlib# Check numpy version
python -c "import numpy; print(numpy.__version__)"
# If output is >=2.x:
pip uninstall numpy
pip install numpy==1.26.4Our venv is intentionally "Frankensteined" because we're on specialized hardware and rely on community-managed wheels rather than professionally maintained ones. Running pip check will likely show warnings like:
albumentations 2.0.8 requires opencv-python-headless, which is not installed.
albucore 0.0.24 requires opencv-python-headless, which is not installed.
tensorflow 2.16.1+nv24.8 has requirement ml-dtypes~=0.3.1, but you have ml-dtypes 0.5.3.
opencv-contrib-python 4.12.0 has requirement numpy<2.3.0,>=2; python_version >= "3.9", but you have numpy 1.26.4.Be aware that when you update or install packages, pip may attempt to resolve these constraints, which can break the environment. Recommended practices:
- Use
--no-depswhen installing wheels from Jetson AI Lab to avoid dependency churn. - Re-run the numpy check above after any package change.
It's possible that opencv-python-headless was installed while upgrading or installing packages, if so:
Check and remove opencv-python-headless
To ensure the correct OpenCV build is used (and to avoid headless conflicts), check and remove opencv-python-headless if present (ensure your venv is active):
# Check if opencv-python-headless is installed
pip show opencv-python-headless || echo "opencv-python-headless not installed"
# Remove it if present
pip uninstall -y opencv-python-headless