• You can now help support WorldwideDX when you shop on Amazon at no additional cost to you! Simply follow this Shop on Amazon link first and a portion of any purchase is sent to WorldwideDX to help with site costs.

Super basic arduino nano data logger (linux)

brandon7861

Loose Wire
I Support WorldwideDX.com!
Nov 28, 2018
2,579
2,861
293
Not sure if this is the right place to put this, but I thought others may find it useful. This is for linux and I have no idea how to do it in windows. Next year, when I forget how to do this and AI costs too much, this will hopefully be here.

A coworker picked me up some cheap AA batteries and I want to compare them to energizers, so I figured I would make a constant current load and monitor the battery voltage over time. I don't have a data logger, but a nano has an ADC input and a USB port. This is how to send the ADC voltage to a CSV file in linux.

Upload this to a nano:

const float ADC_REFERENCE = 5.00;
void setup() {
Serial.begin(115200);
}
void loop() {
int raw = analogRead(A0);
float voltage = raw * (ADC_REFERENCE / 1023.0);
Serial.println(voltage, 4);
delay(1000);
}

The ADC_REFERENCE is the 5v supply (your USB cable voltage). USB hubs can drop voltage. Mine was 4.6v and it has a big affect on accuracy so measure it and enter your actual supply voltage here (just measure between GND and 5v on the arduino pins and put that where 5.00 is). A0 is the ADC pin it is measuring. Its a 10 bit ADC which is where 1023 comes from, its the number of discrete levels it can detect. The 4 after voltage is the number of decimal places it displays. The delay is how often it sends data to the computer in milliseconds. Speeds are limited my ADC clock and USB speeds.

Now close it. You don't want the IDE open when you run the logger and you don't want the logger running when you try to program the nano.

Install the python serial comms package
sudo apt install python3-serial

You should already know this from the arduino IDE, but see what USB port the nano is on
ls /dev/ttyUSB* /dev/ttyACM*
You should see something like "/dev/ttyUSB0". Remember that for the python script.

Make a datalogger directory under your user home dir (or wherever)
mkdir -p ~/data_logger

In that folder, you gotta add a little python script. Start by making the file.
nano ~/data_logger/logger.py

That should open the editor. Paste this in with ctrl+shift+v making sure to get the port right.
import serial
import csv
from datetime import datetime
import os
PORT = "/dev/ttyUSB0"
BAUD = 115200
FILE = "data.csv"
ser = serial.Serial(PORT, BAUD, timeout=2)
file_exists = os.path.exists(FILE)
file_empty = not file_exists or os.path.getsize(FILE) == 0
start_time = datetime.now()
try:
with open(FILE, "a", newline="") as f:
writer = csv.writer(f)
if file_empty:
writer.writerow(["timestamp", "elapsed_seconds", "voltage"])
f.flush()
print("Data logger started.")
print("Writing to:", os.path.abspath(FILE))
print("Press Ctrl+C to stop.\n")
while True:
line = ser.readline().decode("utf-8", errors="ignore").strip()
if not line:
continue
try:
voltage = float(line)
except ValueError:
continue
now = datetime.now()
elapsed = (now - start_time).total_seconds()
writer.writerow([
now.strftime("%Y-%m-%d %H:%M:%S"),
f"{elapsed:.1f}",
f"{voltage:.4f}"
])
f.flush()
print(
now.strftime("%H:%M:%S"),
f"{voltage:.4f} V",
f"({elapsed:.0f} s)"
)
except KeyboardInterrupt:
print("\nLogging stopped.")
finally:
ser.close()
Then save (ctrl+o then Enter) and exit (ctrl+x)

To start logging, use this:
cd ~/data_logger
python3 logger.py

and to stop logging, use ctrl+c

Terminal will look like this:
1787904296366.png


and the csv looks like this:
1787904375731.png

Note how in this test it was reading 0.4596v. I had the ADC pin floating when I did this. Don't expect it to show 0v unless it is actually connected to something that is at 0v. Also, you need to keep it within the nano's supply voltage so resistively divide and scale as appropriate. A zener to absorb spikes and a 1k resistor wouldn't be a bad idea either.

Now I need to make a constant current load and kill some batteries. I'm guessing 100mA is a good starting point. I have this bad habit of starting new projects when others are still incomplete lol
 
Last edited:

I've made some minor improvements if anyone is interested. I will leave the basic version up there and put the better version here.

The new version works like this. Run the python (logging will not start). It does a handshake with the nano and uploads the configuration to the nano so you can change things like supply voltage and new features without reprogramming it each time. The config file looks like this (saved as config.txt in same directory as logger.py):

AUTO_START_ENABLED=false
START_VOLTAGE=2.00
AUTO_STOP_ENABLED=false
STOP_VOLTAGE=1.00
SAMPLE_INTERVAL=1.0
SOUND_ENABLED=true
ADC_REFERENCE=5.11
NEW_FILE_EACH_START=true

Bringing D2 low starts and stops the logging. The configuration allows you to set an optional start and stop logging based on voltage. Less than the stop voltage stops logging and higher than the start voltage starts logging. It also has an option to start a new file each time or to add to the previous file. And if auto stop is set, it has the option to play a notification sound.

code got really long so I will put it in a spoiler.
const int VOLTAGE_PIN = A0;
const int CONTROL_PIN = 2;
// ==================================================
// DEFAULT SETTINGS
// ==================================================
float adcReference = 5.11;
bool autoStartEnabled = false;
bool autoStopEnabled = false;
float startVoltage = 2.00;
float stopVoltage = 1.00;
unsigned long sampleInterval = 1000;
bool soundEnabled = true;

// ==================================================
// LOGGER STATE
// ==================================================
bool logging = false;
bool lastButtonState = HIGH;
bool stableButtonState = HIGH;
unsigned long lastDebounceTime = 0;
unsigned long lastSample = 0;

// ==================================================
// CONFIGURATION RECEIVER
// ==================================================
void receiveConfig() {
String line;
// Tell computer we are ready
Serial.println("READY");
while (true) {
if (Serial.available() == 0) {
continue;
}
line = Serial.readStringUntil('\n');
line.trim();
if (line != "CONFIG") {
continue;
}
// ----------------------------------------------
// Receive configuration
// ----------------------------------------------
while (true) {
if (Serial.available() == 0) {
continue;
}
line = Serial.readStringUntil('\n');
line.trim();

if (line == "END_CONFIG") {
break;
}

if (line.startsWith("AUTO_START_ENABLED=")) {
autoStartEnabled =
line.substring(19) == "true";
}

else if (line.startsWith("START_VOLTAGE=")) {
startVoltage =
line.substring(14).toFloat();
}

else if (line.startsWith("AUTO_STOP_ENABLED=")) {
autoStopEnabled =
line.substring(18) == "true";
}

else if (line.startsWith("STOP_VOLTAGE=")) {
stopVoltage =
line.substring(13).toFloat();
}

else if (line.startsWith("SAMPLE_INTERVAL=")) {
float seconds =
line.substring(16).toFloat();
sampleInterval =
(unsigned long)(seconds * 1000.0);
}

else if (line.startsWith("SOUND_ENABLED=")) {
soundEnabled =
line.substring(14) == "true";
}

else if (line.startsWith("ADC_REFERENCE=")) {
adcReference =
line.substring(14).toFloat();
}
}
Serial.println("CONFIG_OK");
return;
}
}

// ==================================================
// SETUP
// ==================================================
void setup() {
Serial.begin(115200);
pinMode(CONTROL_PIN, INPUT_PULLUP);
delay(1000);
receiveConfig();
}

// ==================================================
// LOOP
// ==================================================
void loop() {
// --------------------------------------------------
// BUTTON
// --------------------------------------------------
bool reading = digitalRead(CONTROL_PIN);

if (reading != lastButtonState) {
lastDebounceTime = millis();
lastButtonState = reading;
}

if ((millis() - lastDebounceTime) > 50) {
if (reading != stableButtonState) {
stableButtonState = reading;

// Button pressed
if (stableButtonState == LOW) {
if (!logging) {
Serial.println("START");
logging = true;
lastSample = millis();
} else {
Serial.println("STOP");
logging = false;
}
}
}
}

// --------------------------------------------------
// READ VOLTAGE
// --------------------------------------------------
int raw = analogRead(VOLTAGE_PIN);
float voltage =
raw * (adcReference / 1023.0);

// --------------------------------------------------
// AUTOMATIC START
// --------------------------------------------------
if (autoStartEnabled &&
!logging &&
voltage >= startVoltage) {
Serial.println("START");
logging = true;
lastSample = millis();
}

// --------------------------------------------------
// LOGGING
// --------------------------------------------------
if (logging) {
unsigned long now = millis();

if (now - lastSample >= sampleInterval) {
lastSample = now;
Serial.println(voltage, 4);

// ------------------------------------------------
// AUTOMATIC STOP
// ------------------------------------------------
if (autoStopEnabled &&
voltage <= stopVoltage) {
Serial.println("STOP");
if (soundEnabled) {
Serial.println("SOUND");
}
logging = false;
}
}
}
}

import serial
import csv
from datetime import datetime
import os
import subprocess
import time
import glob


# ==================================================
# FILES AND SERIAL
# ==================================================

PORT = "/dev/ttyUSB0"
BAUD = 115200

BASE_DIRECTORY = os.path.expanduser("~/data_logger")
CONFIG_FILE = os.path.join(BASE_DIRECTORY, "config.txt")
LOG_DIRECTORY = os.path.join(BASE_DIRECTORY, "logs")
SOUND_FILE = os.path.join(BASE_DIRECTORY, "notify.mp3")

os.makedirs(LOG_DIRECTORY, exist_ok=True)


# ==================================================
# READ CONFIGURATION
# ==================================================

def read_config():

config = {}

with open(CONFIG_FILE, "r") as f:

for line in f:

line = line.strip()

if not line or line.startswith("#"):
continue

if "=" not in line:
continue

key, value = line.split("=", 1)

config[key.strip()] = value.strip()

return config


# ==================================================
# SEND CONFIGURATION TO NANO
# ==================================================

def send_config(ser, config):

print()
print("Sending configuration to Nano...")

ser.write(b"CONFIG\n")

ser.write(
f"AUTO_START_ENABLED={config.get('AUTO_START_ENABLED', 'false')}\n"
.encode()
)

ser.write(
f"START_VOLTAGE={config.get('START_VOLTAGE', '2.00')}\n"
.encode()
)

ser.write(
f"AUTO_STOP_ENABLED={config.get('AUTO_STOP_ENABLED', 'false')}\n"
.encode()
)

ser.write(
f"STOP_VOLTAGE={config.get('STOP_VOLTAGE', '1.00')}\n"
.encode()
)

ser.write(
f"SAMPLE_INTERVAL={config.get('SAMPLE_INTERVAL', '1.0')}\n"
.encode()
)

ser.write(
f"SOUND_ENABLED={config.get('SOUND_ENABLED', 'true')}\n"
.encode()
)

ser.write(
f"ADC_REFERENCE={config.get('ADC_REFERENCE', '5.11')}\n"
.encode()
)

ser.write(b"END_CONFIG\n")

ser.flush()

deadline = time.monotonic() + 5

while time.monotonic() < deadline:

line = (
ser.readline()
.decode("utf-8", errors="ignore")
.strip()
)

if line == "CONFIG_OK":

print("Nano configuration accepted.")

return True

print("ERROR: Nano did not acknowledge configuration.")

return False


# ==================================================
# PLAY SOUND
# ==================================================

def play_sound():

if not os.path.exists(SOUND_FILE):

print("Sound file not found:")
print(SOUND_FILE)

return

print("Playing notification.")

try:

subprocess.Popen(
[
"ffplay",
"-nodisp",
"-autoexit",
"-loglevel",
"quiet",
SOUND_FILE
],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)

except Exception as e:

print("Could not play sound:", e)


# ==================================================
# FIND MOST RECENT LOG
# ==================================================

def find_last_log():

files = glob.glob(
os.path.join(LOG_DIRECTORY, "data_*.csv")
)

if not files:
return None

return max(
files,
key=os.path.getmtime
)


# ==================================================
# OPEN LOG
# ==================================================

def start_logging(new_file_each_start):

if new_file_each_start:

timestamp = datetime.now().strftime(
"%Y%m%d_%H%M%S"
)

filename = os.path.join(
LOG_DIRECTORY,
f"data_{timestamp}.csv"
)

mode = "w"

else:

filename = find_last_log()

if filename is None:

timestamp = datetime.now().strftime(
"%Y%m%d_%H%M%S"
)

filename = os.path.join(
LOG_DIRECTORY,
f"data_{timestamp}.csv"
)

mode = "w"

else:

mode = "a"


log_file = open(
filename,
mode,
newline=""
)

writer = csv.writer(log_file)


# Add header only to a new file

if mode == "w":

writer.writerow([
"timestamp",
"elapsed_seconds",
"voltage"
])

log_file.flush()


print()
print("===================================")
print("LOGGING STARTED")
print("File:", filename)

if mode == "a":
print("Mode: APPENDING")
else:
print("Mode: NEW FILE")

print("===================================")


return log_file, writer, time.monotonic()


# ==================================================
# MAIN
# ==================================================

ser = None
log_file = None
writer = None
start_time = None


try:

config = read_config()

new_file_each_start = (
config.get(
"NEW_FILE_EACH_START",
"true"
).lower() == "true"
)


print("===================================")
print("Data logger")
print("===================================")
print("Configuration:", CONFIG_FILE)


ser = serial.Serial(
PORT,
BAUD,
timeout=1
)


time.sleep(2)


# ------------------------------------------------
# Wait for Nano READY
# ------------------------------------------------

print()
print("Waiting for Nano...")


deadline = time.monotonic() + 5

ready = False


while time.monotonic() < deadline:

line = (
ser.readline()
.decode("utf-8", errors="ignore")
.strip()
)


if line == "READY":

ready = True

print("Nano is ready.")

break


if not ready:

raise RuntimeError(
"Nano did not send READY."
)


# ------------------------------------------------
# Send configuration
# ------------------------------------------------

if not send_config(ser, config):

raise RuntimeError(
"Nano configuration failed."
)


print()
print("Waiting for START...")
print()


# ==================================================
# MAIN SERIAL LOOP
# ==================================================

while True:

line = (
ser.readline()
.decode("utf-8", errors="ignore")
.strip()
)


if not line:
continue


# ----------------------------------------------
# START
# ----------------------------------------------

if line == "START":

if log_file is None:

log_file, writer, start_time = \
start_logging(
new_file_each_start
)

continue


# ----------------------------------------------
# STOP
# ----------------------------------------------

if line == "STOP":

if log_file is not None:

print()
print("LOGGING STOPPED")

log_file.flush()
log_file.close()

log_file = None
writer = None
start_time = None

continue


# ----------------------------------------------
# SOUND
# ----------------------------------------------

if line == "SOUND":

play_sound()

continue


# ----------------------------------------------
# VOLTAGE
# ----------------------------------------------

try:

voltage = float(line)

except ValueError:

print(
"Unknown Nano message:",
line
)

continue


if log_file is None:

continue


elapsed = (
time.monotonic() - start_time
)


timestamp = datetime.now().strftime(
"%Y-%m-%d %H:%M:%S"
)


writer.writerow([
timestamp,
f"{elapsed:.1f}",
f"{voltage:.4f}"
])


log_file.flush()


print(
timestamp,
f"{voltage:.4f} V",
f"({elapsed:.0f} s)"
)


except KeyboardInterrupt:

print()
print("Data logger shutting down...")


except Exception as e:

print()
print("ERROR:")
print(e)


finally:

if log_file is not None:

log_file.flush()
log_file.close()


if ser is not None:

ser.close()


print("Data logger stopped.")
 
Last edited:

dxChat
Help Users
  • No one is chatting at the moment.