Archive for the ‘ESP32’ Category

Programmering 2026

04/10/2026

Den klassiska programmeraren som sitter och skriver, dricker ohälsosam CocaCola, läser/googlar och själv skriver varje rad kod är antagligen död eller håller på att försöka ta sina sista andetag.

I artikeln Moores lag beskrev jag en liten mikrokontroller som jag använde till att avläsa en koldioxidsensor och logga data till ett SD-minne kopplat till mikrokontrollern. Mikrokontrollern har en extremt liten bilskärm som man nästan behöver förstoringsglas för att läsa. För att göra CO2-loggern enkel att använda behöver jag ett program som kör på min laptop och som visar loggad CO2 koncentration, tillåter mig att skicka kommandon till mikrokontrollern etc.

Det finns många alternativ jag kan använda. Det första alternativet som för mig kändes väldigt naturligt skulle vara att använda Lazarus/Pascal som erbjuder ett trevligt grafiskt användarinterface som jag har använt ett antal gånger genom åren. Andra alternativ finns naturligtvis. Med lämpliga biblioterk kunde jag skriva ett grafiskt program i språket C eller i C++ men jag har inte kodat användarinterface i dessa språk vilket skulle betyda en hel del goolande och läsande.

Av en slump stötte jag på ett intressant verktyg baserat på öppen källkod: Processing. Processing bygger på Java men detta ursprung syns egentligen inte utan Processing påminner om Arduino IDE som jag känner väl från tidigare. Det största problemet med ett nytt verktyg som t.ex. Processing är att läsa sig in på vad det egentligen gör och hur det används samt ofta ny terminologi som grumlar till de första timmarna då man försöker förstå dokumentationen som plötsligt talar ett främmande språk.

Min medhjälpare AI

Jag beslöt att i stället för att sätta igång och koda i Processing så låter jag AI göra jobbet genom att ange vad jag vill ha gjort.

Mikrokontrollern ESP32-C3_OLED är kopplad till min PC över en serielinje som kör på 115200 baud. Loggade värden skrivs ut till serieporten med 5s intervall och med 15 minuters intervall loggas data till ett SD-minne på mikrokontrollern. Jag vill ha en applikation som körs på min Linux PC som i grafisk form visar inkommande data, tillåter mig att starta, stoppa och radera mätningar samt i grafisk form kontinuerligt visa CO2 trenden d.v.s. hur CO2 koncentrationen ökar/minskar.

Min diskussion med AI löpte på följande sätt:

Programmeraren (alltså jag):

Can you write me a Processing sketch that reads serial data from Arduino and writes the serial data to a window on the PC?

AI respons:

Here is a Processing sketch that reads serial data from an Arduino and displays it live in a graphical window on your PC.

import processing.serial.*;
Serial myPort; // Create object from Serial class
String inString = ""; // Input string from serial port
PFont f; // Font for displaying text
void setup() {
size(600, 400); // Window size (width, height)
// Print a list of available serial ports to the console to help find the correct one
printArray(Serial.list());
// Open the port your Arduino is connected to.
// Change [0] to match your Arduino's index in the list if needed!
String portName = Serial.list()[0];
myPort = new Serial(this, portName, 9600);
// Read bytes until a newline character ('\n') is received
myPort.bufferUntil('\n');
// Create a simple font for the text display
f = createFont("Arial", 16, true);
textFont(f);
}
void draw() {
background(40); // Dark gray background
fill(255); // White text color
textSize(18);
// Title on the window
text("Arduino Serial Data Monitor", 20, 30);
// Display the latest received string on the window
textSize(24);
fill(0, 255, 128); // Neon green text for data
text(inString, 20, 100);
}
// This function runs automatically when new serial data ends with a newline ('\n')
void serialEvent(Serial myPort) {
inString = myPort.readStringUntil('\n');
if (inString != null) {
inString = trim(inString); // Remove whitespace and extra newlines
}
}

Resultatet blev ett enkelt grafiskt fönster nedan.

Fig. 1 Resultatet av det första försöket. Inte mycket att hänga i julgranen men programmet visar ett fönster på PC-skärmen, sätter upp seriekommunikationen korrekt och skriver ut ett mätvärde i taget helt enligt vad jag bad om.

Appen saknar helt möjligheter att från PC:n skicka textkommandon till mikrokontrollern. Saken kan fixas genom att be AI förbättra koden.

Programmeraren:

Add a field to input commands to send to the running Arduino app.

AI:

To add an input field for sending commands back to the Arduino, you can use Processing’s built-in keyPressed() function to capture typing and send the string over the serial port when you press Enter.
Here is the updated Processing sketch with a visual input box and keyboard text capture:

import processing.serial.*;
Serial myPort; // Create object from Serial class
String inString = ""; // Input string from serial port
String outString = ""; // Text being typed to send to Arduino
String lastSent = ""; // Remembers the last command sent
PFont f; // Font for displaying text
void setup() {
size(600, 400); // Window size (width, height)
// Print list of available serial ports to the console
printArray(Serial.list());
// Open the port your Arduino is connected to
String portName = Serial.list()[0]; // Change '0' if your Arduino is at a different index
myPort = new Serial(this, portName, 9600);
// Read bytes until a newline character ('\n') is received
myPort.bufferUntil('\n');
f = createFont("Arial", 16, true);
textFont(f);
}
void draw() {
background(40); // Dark gray background
// SECTION 1: Display Incoming Data
fill(255);
textSize(16);
text("Incoming From Arduino:", 30, 40);
fill(0, 255, 128); // Neon green for incoming data
textSize(24);
text(inString, 30, 80);
// SECTION 2: Input Field for Outgoing Commands
stroke(100);
fill(60);
rect(30, 180, 540, 45, 5); // Draw the text input box
fill(255);
textSize(14);
text("Type command and press ENTER to send:", 30, 165);
// Display the text currently being typed inside the box
fill(255, 255, 0); // Yellow text for typing
textSize(18);
text(outString + (frameCount % 30 < 15 ? "|" : ""), 40, 210); // Adds a blinking cursor
// SECTION 3: Confirmation of last sent command
if (!lastSent.equals("")) {
fill(180);
textSize(14);
text("Last sent command: \"" + lastSent + "\"", 30, 260);
}
}
// Captures keyboard input for the command box
void keyPressed() {
if (key == ENTER || key == RETURN) {
// Send the typed string to the Arduino, adding a newline character
myPort.write(outString + "\n");
lastSent = outString; // Save to display on screen
outString = ""; // Clear the input field for the next command
}
else if (key == BACKSPACE) {
// Remove the last character if backspace is pressed
if (outString.length() > 0) {
outString = outString.substring(0, outString.length() - 1);
}
}
else if (key != CODED && key != ESC) {
// Append valid typed characters to our outgoing string
outString += key;
}
}
// Automatically runs when incoming data hits a newline ('\n')
void serialEvent(Serial myPort) {
inString = myPort.readStringUntil('\n');
if (inString != null) {
inString = trim(inString); // Clean up whitespace
}
}

Fig. 2 Jag kan nu skicka kommandon till mikrokontrollern men t.ex. kommandot help producerar flera rader text vilket gör att jag endast ser den sista raden i appen ovan … vilket inte är till speciellt stor hjälp.

Programmeraren:

Make the incoming data area larger to fit the twenty last lines received from Arduino.

AI:

Here is the updated Processing sketch. It replaces the single-line display with a scrolling string array that stores and displays the last 20 lines received from the Arduino, formatting it like a classic terminal window.

import processing.serial.*;
Serial myPort; // Create object from Serial class
String[] lineHistory = new String[20]; // Array to store the last 20 lines
String outString = ""; // Text being typed to send to Arduino
String lastSent = ""; // Remembers the last command sent
PFont f; // Font for displaying text
void setup() {
size(700, 600); // Larger window size (width, height) to fit history
// Initialize the history array with empty strings
for (int i = 0; i < lineHistory.length; i++) {
lineHistory[i] = "";
}
// Print list of available serial ports to the console
printArray(Serial.list());
// Open the port your Arduino is connected to
String portName = Serial.list()[0]; // Change '0' if your Arduino is at a different index
myPort = new Serial(this, portName, 9600);
// Read bytes until a newline character ('\n') is received
myPort.bufferUntil('\n');
f = createFont("Courier New", 14, true); // Monospace font fits terminal history best
textFont(f);
}
void draw() {
background(30); // Dark gray background
// SECTION 1: Display Incoming Data History (Terminal Box)
fill(255);
textSize(14);
text("Incoming From Arduino (Last 20 lines):", 30, 30);
stroke(80);
fill(15);
rect(30, 45, 640, 410, 5); // Terminal history background box
// Draw the 20 lines inside the terminal box
fill(0, 255, 128); // Neon green for incoming data
int startY = 65; // Vertical starting position for the first line
int lineHeight = 20; // Space between lines
for (int i = 0; i < lineHistory.length; i++) {
text(lineHistory[i], 45, startY + (i * lineHeight));
}
// SECTION 2: Input Field for Outgoing Commands
stroke(100);
fill(50);
rect(30, 500, 640, 45, 5); // Draw the text input box
fill(255);
textSize(14);
text("Type command and press ENTER to send:", 30, 485);
// Display the text currently being typed inside the box
fill(255, 255, 0); // Yellow text for typing
text(outString + (frameCount % 30 < 15 ? "_" : ""), 45, 528); // Terminal-style blinking cursor
// SECTION 3: Confirmation of last sent command
if (!lastSent.equals("")) {
fill(180);
textSize(12);
text("Last sent: \"" + lastSent + "\"", 30, 570);
}
}
// Captures keyboard input for the command box
void keyPressed() {
if (key == ENTER || key == RETURN) {
// Send the typed string to the Arduino, adding a newline character
myPort.write(outString + "\n");
lastSent = outString; // Save to display on screen
outString = ""; // Clear the input field for the next command
}
else if (key == BACKSPACE) {
// Remove the last character if backspace is pressed
if (outString.length() > 0) {
outString = outString.substring(0, outString.length() - 1);
}
}
else if (key != CODED && key != ESC) {
// Append valid typed characters to our outgoing string
outString += key;
}
}
// Automatically runs when incoming data hits a newline ('\n')
void serialEvent(Serial myPort) {
String inString = myPort.readStringUntil('\n');
if (inString != null) {
inString = trim(inString); // Clean up whitespace and newlines
// Shift all elements in history array up by 1 index
for (int i = 0; i < lineHistory.length - 1; i++) {
lineHistory[i] = lineHistory[i + 1];
}
// Add the newest line to the very bottom of the array
lineHistory[lineHistory.length - 1] = inString;
}
}

Fig. 3 De tjugo sista mätvärdena presenteras nu på PC-bildskärmen.

Resultatet är helt OK. Jag ser vad som händer nere på mikrokontrollern men det skulle vara trevligt med en grafisk display över mätvärdena. Det är lättare att med ett ögonkast se vad som händer i grafisk form än som en rad siffror.

Programmeraren:

Add a graphical display of single numerical values to the left of the scrolling text window. The size is 200×200 pixels. Scale the Y-axis 400-1500 and the X-axis as number of measurement with ticks 100 measurements apart.

AI:

Here is the updated Processing sketch. It expands the window to place a 200×200 pixel graph directly to the left of the terminal window.
The graph is designed to automatically parse incoming numeric text, plot it sequentially over time, and draw dynamic X-axis tick markers every 100 measurements. It also clips the plotted points to the exact requested 400 to 1500 Y-axis range.

import processing.serial.*;
Serial myPort;
String[] lineHistory = new String[20];
String outString = "";
String lastSent = "";
PFont f;
// Graphing Variables
FloatList graphData; // Stores the history of numerical measurements
int graphX = 30; // Left edge of the 200x200 graph
int graphY = 45; // Top edge of the 200x200 graph
int graphW = 200; // Graph Width
int graphH = 200; // Graph Height
float minVal = 400; // Y-axis minimum scale
float maxVal = 1500; // Y-axis maximum scale
void setup() {
size(930, 600); // Expanded width (700 + 230) to comfortably fit the graph on the left
graphData = new FloatList();
for (int i = 0; i < lineHistory.length; i++) {
lineHistory[i] = "";
}
printArray(Serial.list());
String portName = Serial.list()[0];
myPort = new Serial(this, portName, 9600);
myPort.bufferUntil('\n');
f = createFont("Courier New", 14, true);
textFont(f);
}
void draw() {
background(30);
// -----------------------------------------------------------------
// SECTION 1: GRAPHICAL DISPLAY (200x200 Pixels)
// -----------------------------------------------------------------
fill(255);
textSize(14);
text("Live Plot:", graphX, 30);
// Draw Graph Grid Background
stroke(80);
fill(15);
rect(graphX, graphY, graphW, graphH);
// Draw Y-Axis Labels (400 to 1500)
fill(150);
textSize(10);
textAlign(RIGHT, CENTER);
text("1500", graphX - 5, graphY);
text("950", graphX - 5, graphY + graphH/2);
text("400", graphX - 5, graphY + graphH);
textAlign(LEFT, BASELINE); // Reset alignment
// Draw X-Axis Ticks (every 100 measurements)
int totalMeasurements = graphData.size();
if (totalMeasurements > 0) {
stroke(60);
textSize(9);
fill(120);
// Find the first multiple of 100 visible in the current view window
int startTick = (max(0, totalMeasurements - graphW) / 100) * 100;
if (startTick < totalMeasurements - graphW) {
startTick += 100;
}
for (int t = startTick; t <= totalMeasurements; t += 100) {
// Map the measurement sequence number to the pixel X position
int historyIndexFromRight = totalMeasurements - t;
float tickPixelX = (graphX + graphW) - historyIndexFromRight;
if (tickPixelX >= graphX && tickPixelX <= graphX + graphW) {
line(tickPixelX, graphY, tickPixelX, graphY + graphH); // Gridline
text(t, tickPixelX - 5, graphY + graphH + 12); // Measurement number label
}
}
}
// Plot the Data Line
stroke(0, 255, 255); // Cyan color line for plot data
strokeWeight(1.5);
noFill();
beginShape();
// Display only the data that fits inside the 200px width limit
int displayCount = min(graphData.size(), graphW);
int startIndex = graphData.size() - displayCount;
for (int i = 0; i < displayCount; i++) {
float val = graphData.get(startIndex + i);
// Constrain data values to the requested scale limits
val = constrain(val, minVal, maxVal);
// Convert numerical space (400-1500) to Pixel Space coordinates
float plotX = graphX + i + (graphW - displayCount);
float plotY = map(val, minVal, maxVal, graphY + graphH, graphY);
vertex(plotX, plotY);
}
endShape();
strokeWeight(1); // Reset stroke weight
// -----------------------------------------------------------------
// SECTION 2: SCROLLING TEXT WINDOW (Shifted to the right)
// -----------------------------------------------------------------
int textWindowX = 260; // Pushed right to clear the 200px graph + margins
fill(255);
textSize(14);
text("Incoming From Arduino (Last 20 lines):", textWindowX, 30);
stroke(80);
fill(15);
rect(textWindowX, 45, 640, 410, 5);
fill(0, 255, 128);
int startY = 65;
int lineHeight = 20;
for (int i = 0; i < lineHistory.length; i++) {
text(lineHistory[i], textWindowX + 15, startY + (i * lineHeight));
}
// -----------------------------------------------------------------
// SECTION 3: INPUT FIELD & UTILITIES (Spans the bottom area)
// -----------------------------------------------------------------
stroke(100);
fill(50);
rect(30, 500, 870, 45, 5);
fill(255);
textSize(14);
text("Type command and press ENTER to send:", 30, 485);
fill(255, 255, 0);
text(outString + (frameCount % 30 < 15 ? "_" : ""), 45, 528);
if (!lastSent.equals("")) {
fill(180);
textSize(12);
text("Last sent: \"" + lastSent + "\"", 30, 570);
}
}
void keyPressed() {
if (key == ENTER || key == RETURN) {
myPort.write(outString + "\n");
lastSent = outString;
outString = "";
}
else if (key == BACKSPACE) {
if (outString.length() > 0) {
outString = outString.substring(0, outString.length() - 1);
}
}
else if (key != CODED && key != ESC) {
outString += key;
}
}
void serialEvent(Serial myPort) {
String inString = myPort.readStringUntil('\n');
if (inString != null) {
inString = trim(inString);
// 1. Log Raw String to Console Feed
for (int i = 0; i < lineHistory.length - 1; i++) {
lineHistory[i] = lineHistory[i + 1];
}
lineHistory[lineHistory.length - 1] = inString;
// 2. Parse Numeric Value for Graphing
try {
float parsedValue = float(inString);
if (!Float.isNaN(parsedValue)) {
graphData.append(parsedValue);
}
} catch (Exception e) {
// Keeps going safely if text lines contain alpha commands instead of integers
}
}
}

Fig. 4 Resultatet blev en app som kör under Linux, Windows och Mac som visar inkommande text från mikrokontrollern, tillåter mig att kontrollera mikrokontrollern med en serie kommandon samt visar en grafisk bild av CO2-nivån.

Notera! Jag producerade flera hundra rader kod för ett verktyg jag aldrig tidigare har använt utan att själv ha skrivit en enda rad kod. Total tid till den sista helt fungerande varianten var under femton minuter. Själva beskrivningen av vad jag gjorde krävde betydligt mera tid än själva ”kodandet”.

Min personliga uppfattning är att framtidens kodare är en person med en klar bild av vad hen vill ha och som är kapabel att i klartext ange det önskade resultatet. En typisk arbetsgång är att förbättra programmet i små steg, inte att försöka skriva en specifikation över många sidor. Varje steg testas innan följande detalj läggs till.

Moore’s lag

29/09/2026

Sedan slutet av 1700-talet har man kunnat följa en nästan ofattbar utveckling på datateknikens område. Charles Babbage visade att man i princip kunde skapa en mekanisk programmerbar dator. En praktisk mekanisk dator skulle ha varit mycket stor, tänk en fotbollsplan, och samtidigt ur modern synvinkel extremt långsam. Det har visat sig att Babbages mekaniska dator var korrekt konstruerad och med tillräcklig finansiering och långvarig utveckling skull den ha varit möjlig att bygga. Babbage fick finansiering för att bygga datorn 1823 men den färdigställdes aldrig.

Matematikern Ada Lovelace (1815-1852) uppfattas som världens första programmerare. Tillsammans med Babbage utvecklade de idéer kring program, variabler och vilkorliga hopp i ett datorprogram, idéer som fortfarande är gångbara.

Senare studerade man Babbages idéer och insåg att Babbages mekaniska dator skulle vara mycket enklare att implementera i elektronisk form. De första elektroniska datorerna använde mekaniska reläer och elektronrör men då transistorn uppfanns kunde en mycket snabb miniatyrisering inledas, först så att elektronrör byttes mot transistorer men relativt snabbt lärde man sig att lägga in många transistorer på samma kiselbit d.v.s. man började skapa integrerade kretsar. Efter några år had man lärt sig att skapa centralenheter för enkla datorer som innehöll några tusen transistorer, Utvecklingsprocessen fortgår fortfarande men på en kiselbit ryms idag miljarder transistorer.

Det har visat sig att ”kapaciteten” hos en dator fördubblades på ca 2-3 år. Då utvecklingen fortgår under många år kommer kapacitetsökningen att vara ofantlig. Fördubblingen av kapaciteten under en given tidrymd kallas Moores lag.

Då vi ser vad en fördubbling följd av en fördubbling följd av … leder till kommer att tänka på legenden om uppfinnaren av schackspelet Sissa Ibn Dahir som av kungen bad om en enkel belöning för det fantastiska spelet … sätt ett sädeskon på den första rutan, två på den andra rutan, fyra på följande o.s.v. så att mängden sädeskorn fördubblas för varje varje följande ruta. Resultatet blev att det på den sista sextiofjärde rutan fanns ungefär 1844600000000000000 sädeskorn och kungen gick i konkurs.

Det är skäl att komma ihåg att Moores lag inte går att tillämpa på godtycklig teknik. En förutsättning för att Moores lag har fungerat under lång tid är en teknisk utveckling av miniatyrisering kopplad till extremt effektiv serieproduktion. Det är alltså inte möjligt att se samma utveckling gällande t.ex. vindkraft där man startar från en 2000 år gammal teknik och där miniatyrisering inte är möjlig eftersom man samlar in vindenergi över ett stort område.

Under 1960-talet hade man utvecklat programmerbara datorer som gick att programmera i programmeringsspråk som fortfarande används t.ex. FORTRAN. Kapaciteten var mycket mindre än kapaciteten i en modern mobiltelefon. Hjärnan i en modern mobiltelefon skulle med glädje ha utnyttjats som centraldator för ett helt universitet på 1960-talet.

För några veckor sedan råkade jag se en reklam för ett litet mikrokontrollerkort d.v.s. egentligen en liten dator med storleken 25 * 20 mm som lätt kan kopplas till en konventionell dator för programmering. Det speciella med just den här mikrokontrollern är att den har en inbyggd bildskärm som i praktiken kan visa två korta rader text. Jag kunde naturligtvis inte motstå frestelsen att prova den så jag beställde tio stycken till ett styckepris på 1.52 Euro.

Fig. 1 Mikrokontrollern ESP32-C3 OLED.

För att ha något vettigt att göra med min lilla kontroller beslöt jag att utnyttja en koldioxidsensor jag råkade ha liggande i miljonlådan och bygga en koldioxidmätare som på den pyttelilla bildskärmen kontinuerligt kunde visa koldioxidhalten i luften.

Det gick väldigt snabbt och enkelt att få fram kod för att avläsa sensorn och skriva ut det mätta värdet på bildskärmen. Jag gissar att jag fick det hela att fungera på mindre än en halv timme. Då kommer naturligtvis följande fråga. Om jag vill följa med hur coldioxidhalten förändras under en längre tidsperiod så vore det kul att kunna lagra mätvärdena på ett mikro SD minneskort.

Följande steg visade sig vara något besvärligare. Mikrokontrollern är dåligt dokumenterad. Det visade sig att jag försökte koppla in SD kortet på In/Ut pinnar som användes internt av mikrokontrollern. Efter diverse sökande hittade jag ovanstående bild som visar att det inte lönar sig att använda GPIO6 och GPIO5 eftersom de redan används av bildskärmen som är kopplad via en såkallar I2C seriedatabuss.

Då jag nu kunde skriva CO2 mätvärden till SD-minneskort visade det sig att mätningarna ”drev” tidsmässigt väldigt mycket d.v.s. det gick inte att tvinga fram mätningar med ett enkelt regelbundet mätintervall på t.ex. 5 s per mätning. Lösningen var att starta en timer (klockfunktion) som jag ställde in så att den skulle meddela att det var tid att mäta med 5 s intervall. Arbetsslingan ”loop” i programmet måste i viss mån modifieras så att mätintervallet inte styrdes av väntefunktionen längst ner i programmet. Funktionen delay() väntar en tiondels sekund innan huvudslingan loop() startas på nytt automatiskt. Väntefunktionen är relativt kort för att jag inte skall behöva vänta på kommandon jag skriver över USB serieporten. Att sköta mätningarna via interrupt visade sig fungera bra.

Eftyer ett antal successiva förbättringar kom jag fram till ett mätprogram som, om mikrokontrollern är kopplad till en dator, kan tömma mätfilen, skriva ut innehållet i filen, stoppa och återstarta mätningarna samt att starta mätningarna så att det bokförs tid/datum då mätningarna startas. Tanken är att jag senare enkelt skall kunna rita ut grafer över hur CO2 halten förändras över lång tid kanske några veckor.

Fig. 2 Det färdiga systemet kopplat på ett prototypkort. Längst ner på bilden ses USB-anslutningen till min dator. USB matar den ström mikrokontrollern behöver. Man ser den pyttelilla bildskärmen på processorkortet. Strax ovanför mikrokontrollern syns ett litet kort från vilket det sticker ut ett SD-minnskort till vänster. Överst finns en silverfärgad enhet som är en optisk koldioxidmätare.

More’s lag igen

Då man ser på den extremt lilla mikrokontrollern så kan man plötsligt reagera på hur otroligt mycket tekniken har gått framåt. Operativsystemet UNIX utvecklades på minidatorn PDP-8 som har en ordlängd på 12-bitar. Ordlängd betyder att en instruktion har längden 12-bitar i det binära systemet där det finns endast två siffror 0 och 1. I en 12 bitars minnesplats kan man lagra tal mellan 0 och 4095.

Den maximala minnesstorleken i form av magnetkärnor var 32 000 12-bitars minnesplatser. Program kunde lagras på pappersremsa eller på magnetband men PDP-8 frontpanel, se fig 3, användes till att mata in ett kort program för att läsa in program från t.ex. hålremsa. Program skrevs i assembler d.v.s. direkt i symbolisk maskinkod. Vi kan jämföra denna PDP-8 minidator från 1965 som kostade 18500 dollar med min lilla mikrokontroller. PDP-8 hade en instruktionscykel på 1.5 mikrosekunder vilket motsvarar ungefär 600 000 minnesoperationer per sekund.

Fig. 3 Bilden visar en PDP-8 minidator från datormusét i Stuttgart. Notera att kringutrustningen i form av läsare av hålremsa, Teletype terminal och eventuellt en bandstation saknas.

Fig. 4 Dennis Ritchie och Ken Thompson, utvecklarna av UNIX, arbetar på en något mera kraftfull PDP-11 minidator. I bilden syns två Teletype terminaler. Man se mängder av rullar med hålremsor. Uppe till vänster ser man en serie magnetband.

Mikrokontroller ESP32-C3 är en RISC processor som är konstruerad för att köra en operation per klockcykel. Ett minnesord i ESP32-C3 är 32-bitar långt d.v.s. 2.6 ggr. längre an PDP-8 12-bitar. ESP32-C3 kör på en klockfrekvens på 160 MHz d.v.s. ungefär 270 ggr snabbare än PDP-8. Programminnet i mikrokontrollern är maximalt ca. 1 000 000 miljon ord jämfört med PDP-8 32 000 ord skillnaden är ungefär 30 ggr till mikrokontrollerns fördel.

Prisskillnaden mellan mikrokontrollerns 1.52 Euro och PDP-8 pris på 18500 dollar är svår att uppskatta. Om man uppskattar vad PDP-8 skulle ha kostat i dagens pris får man alltså lägga till ungefär en faktor tio d.v.s. priset omräknat till modernt pris blir ungefär 200 000 dollar. PDP-8 skulle alltså ha varit ungefär 130 000 ggr dyrare än mikrokontrollern ESP32-C3.

Om vi kombinerar hur mycket mera kapacitet den lilla ESP32-C3 har än PDP-8 på vilken den första Unix varianten skrtevs, Unix är förebilden för Linux som nu dominerar världens datorer, så kommer vi fram till en kapacitetsförbättring tekniskt och prismässigt på ungefär 1 000 000 000 ggr.

Uppskattningen av en kapacitetsförbättring pris/snabbhet/minne etc. på en miljard gånger är, tycker jag, ett bra exempel på vad Moore’s lag är!

Programkoden till CO2-mätaren finns nedan. Notera att det är ett snabbt hack där det bl.a. finns funktioner relaterade till SD-minnet som inte används i detta skede. Programmet innehåller mycket kommentarer d.v.s. det torde vara möjligt att förstå koden utan någon djup erfarenhet av programmering.

Några kommentarer till koden:

En rad som börjar med // är en kommentar avsedd att läsas av människor. En kommentar körs inte av processorn.

En funktion har utseendet namn()

Då man hanterar hårdvara behöver man veta vilka ”ben” på processorn man vill styra. De flesta ben är logiskt kopplade till generell In/Ut funktionalitet ofta kallad GPIOxx där xx är in/ut enheten nummer se fig. 1 ovan där det i bilden anges vilka GPIO som är kopplade till vilka ben.

Det är mycket lättare att läsa ett program om man hänvisar till t.ex. den inbyggda blå lysdionen med LED_BUILTIN i stället för In/Ut funktionens nummer som är 8. Jag kan alltså definiera att på varje plats i programmet där jag styr lysdioden så byter kompilatorn ut LED_BUILTIN mot talet 8 exempelvis kan vi definiera:

#define LED_BUILTIN 8

På motsvarande sätt har jag gett namn på in/ut ben för att controllera bildskärmen och SD minneskortet.

Funktionen setup() körs alltid automatiskt då processorn startar. Funktionen används för att ställa in önskad funktionalitet t.ex. serieportens hastighet innan processorn börjar köra sin eviga slinga loop() . Olika funktioner, i vårt fall i huvudsak kopplade till kontroll av SD minnet, finns definierade mellan setup() och loop().

// Name: ESP32_Mini_OLED.ino
//
// This is a hoppy project to test a ESP32-C3 with built in ultra miniature
// LCD display. A C8-CO2-5K CO2 sensor is used to produce some data to display. The CO2
// sensor produces pulse width modulated square wave pulses. The sensor is connected
// to GPIO 0.
// The result is written to USB serial and written to the miniature
// LCD display of the type U8G2_SSD1306_72X40_ER_F_HW_I2C.
// Measurement data is logged to a SD memory card if available.
//
// The display uses the pins SDA_PIN = 5 and SCL_PIN = 6 for I2C communication. If several
// devices use I2C then make sure they use different I2C addresses.
// Notice that there is no automatic line feed and everything has to be
// written to positions set by program setCursor() or drawString()
//
// The SD memory is connected through a SPI interface. The connections are:
// CS_SD 3
// MOSI_SD 7
// MISO_SD 2
// CLK_SD 9
//
// To connect several devices to SPI use different pins as chip select. MOSI, MISO and CLK
// are common to all units using the SPI interface.
//
// The internal bnlue LED is connectewd to LED_BUILTIN (GPIO8). It is blinking when the system runs normally.
// The Blue led is on when LED_BUILTIN = LOW and off when LED_BUILTIN = HIGH.
//
// Measurements are done once every 5 seconds and logged to screen. Logging to
// SD memory is done once every 15 minutes controlled by the cnt=180 counter variable
// in line.
// The number of the measurement is logged in the variable meas. To get the actual time in
// seconds since start multiply the first number of the number pair with 5.
//
// Remember that:
// 60 s --> 1 minute
// 3600 s --> 1 hour
// 86400 s --> 1 day
//
// Example when was the measurement 896 , 838 taken?
// The first number 896 is the number of the measurement. Measurements are done at 5 seconds
// intervals which means that the measurement was done 5*896 seconds since start = 4480 seconds
// since measurement start roughly = 1 hour 15 minutes sincew start.
//
// Adding USB-serial control to start/stop measurements, clear the /log.txt file
// and dump the /log.txt to the serial monitor.
// file to the serial monitor.
// Commands:
// HELP print a list of known kommands.
// LOG print the contents of the log file.
// CLEAR erase the log file and generate a new one.
// START Comment including start of measurement.
// PAUSE pause measurements to dump file to USB serial.
// In PAUSE mode it is safe to remove the SD memory.
// CONTINUE contyinue measurements after PAUSE.
//
// Measurements are controlled by interrupt to ensure a reasonably accurate clock
// when no RTC is in use. The measurement interval is set to 5 seconds ensuring
// that the previous measurement is processed before starting the next measurement.
//
// Lars Silen 2026. Feel free to use on your own risk.
#include <Arduino.h>
#include <Wire.h>
#include <U8g2lib.h>
#include "FS.h"
#include <SPI.h>
#include <SD.h>
#include <sd_defines.h>
#include <sd_diskio.h>
// Needed for the real time clock and temperature
#include <Wire.h>
//#include <RTClib.h> // Possibly added later.
// Initialize the U8g2 library for the 72x40 SSD1306 display using hardware I2C
// Pin connections on "The Egg" ESP32-C3: SDA = GPIO5, SCL = GPIO6 I2C address 0x78 (actually 2*0x3C)
U8G2_SSD1306_72X40_ER_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
#define SDA_PIN 5
#define SCL_PIN 6
#define CO2_PIN 0
#define CS_SD 3
#define MOSI_SD 7
#define MISO_SD 2
#define CLK_SD 9
#define LED_BUILTIN 8
#define FALSE 0
#define TRUE 1
// If test is defined debugging information is written to USB serial.
//#define TEST
// No real time clock is presently used. THis means that when we start measurements and logging CO2
// we need to enter the start time of the measurements. The clock in the microcontroller is probably
// accurate enough to measure for weeks or months without major timing errors. A RTC uses its own
// battery to keep the clock running even when the micro controller is powered down.
//RTC_DS3231 rtc;
unsigned long duration;
int cnt = -1; // We want to make measurements stable before starting to log
int meas = 0; // Number of measurements since start
int shortCnt = 0; // Used for update interval on LCD
uint8_t cardType = 0; // Not known yet.
int SD_available = FALSE;
String line="Dummy";
String inputLine="";
int timerTrig=FALSE;
int tmpPause = FALSE;
const int ledPin = LED_BUILTIN;
hw_timer_t *MyTimer = NULL;
void IRAM_ATTR onTimer() {
timerTrig = TRUE;
}
void setup(void) {
// Set up control pins for the display
Wire.begin(SDA_PIN, SCL_PIN);
u8g2.setI2CAddress(0x78); // Actually 2*0x3C
u8g2.begin();
// Allow debugging asnd control over USB serial
Serial.begin(115200);
while (!Serial); // Ensure that USB serial is running.
// We use pin 0 to read the pulse width from the C8-CO2-5K CO2 sensor
pinMode(CO2_PIN, INPUT);
// Set up SPI for the SD memory
SPI.begin(CLK_SD, MISO_SD, MOSI_SD, CS_SD);
if (!SD.begin(CS_SD)) {
Serial.println("Card Mount failed!");
SD_available = FALSE;
} else {
Serial.println("SD Card initialized.");
Serial.print("SD Card Type: ");
Serial.println(cardType);
}
pinMode(LED_BUILTIN, OUTPUT); // LED is on when the pin is LOW off when HIGH.
cardType = SD.cardType(); // Card types not checked at the moment. Max size 32 GB.
// Initialize the timer with a 1 MHz frequency (1 tick = 1 microsecond)
MyTimer = timerBegin(1000000);
// Attach the interrupt service routine (ISR). The ISR should be kept very short. We simply set the
// state of the timerTrig to TRUE indicating that a measurement should be taken. The time accuracy of
// the measurement is in the order of one second because the frequency of the PWM output from
// the sensor is roughly 1 Hz. For a slowly changing signal like the CO2 concentration this is perfectly OK.
timerAttachInterrupt(MyTimer, &onTimer);
// Set the alarm limit in microseconds (e.g., 5000,000 us = 5 s) and enable auto-reload (true)
// As can be seen in the main loop below measurements are stored to SD memory at 15 minutes intervals.
// The measurements done at 5s intervals are only used to update the display and USB serial.
timerAlarm(MyTimer, 5000000, true, 0);
// Start the timer
timerStart(MyTimer);
}
// Presently not used.
void listDir(fs::FS &fs, const char * dirname, uint8_t levels){
Serial.printf("Listing directory: %s\n", dirname);
File root = fs.open(dirname);
if(!root){
Serial.println("Failed to open directory");
return;
}
if(!root.isDirectory()){
Serial.println("Not a directory");
return;
}
File file = root.openNextFile();
while(file){
if(file.isDirectory()){
Serial.print(" DIR : ");
Serial.println(file.name());
if(levels){
listDir(fs, file.name(), levels -1);
}
} else {
Serial.print(" FILE: ");
Serial.print(file.name());
Serial.print(" SIZE: ");
Serial.println(file.size());
}
file = root.openNextFile();
}
}
// Presently not used.
void createDir(fs::FS &fs, const char * path){
Serial.printf("Creating Dir: %s\n", path);
if(fs.mkdir(path)){
Serial.println("Dir created");
} else {
Serial.println("mkdir failed");
}
}
// Presently not used.
void removeDir(fs::FS &fs, const char * path){
Serial.printf("Removing Dir: %s\n", path);
if(fs.rmdir(path)){
Serial.println("Dir removed");
} else {
Serial.println("rmdir failed");
}
}
// Dump CO2 measurements to USB serial.
void readFile(fs::FS &fs, const char * path){
Serial.printf("Reading file: %s\n", path);
File file = fs.open(path);
if(!file){
Serial.println("Failed to open file for reading");
return;
}
Serial.print("Read from file: ");
while(file.available()){
Serial.write(file.read());
}
file.close();
}
// Log measurements to the SD memory. If the file doesn't exist create it.
void writeFile(fs::FS &fs, const char * path, const char * message){
Serial.printf("Writing file: %s\n", path);
File file = fs.open(path, FILE_WRITE);
if(!file){
Serial.println("Failed to open file for writing");
return;
}
if(file.print(message)){
Serial.println("File written");
} else {
Serial.println("Write failed");
}
file.close();
}
// Write measurements to the end of the file (append).
void appendFile(fs::FS &fs, const char * path, const char * message){
Serial.printf("Appending to file: %s\n", path);
File file = fs.open(path, FILE_APPEND);
if(!file){
Serial.println("Failed to open file for appending");
return;
}
if(file.print(message)){
Serial.println("Message appended");
} else {
Serial.println("Append failed");
}
file.close();
}
// Not presently used.
void renameFile(fs::FS &fs, const char * path1, const char * path2){
Serial.printf("Renaming file %s to %s\n", path1, path2);
if (fs.rename(path1, path2)) {
Serial.println("File renamed");
} else {
Serial.println("Rename failed");
}
}
// Clear the file.
void deleteFile(fs::FS &fs, const char * path){
Serial.printf("Deleting file: %s\n", path);
if(fs.remove(path)){
Serial.println("File deleted");
} else {
Serial.println("Delete failed");
}
}
// The main loop of the program. A program in a micro controller never stops. When the end of
// the main loop is reached all local variables, local to loop() are cleared and execution starts
// from the beginning of the loop(). Global variables are not cleared.
void loop(void) {
digitalWrite(LED_BUILTIN, HIGH);
// Measurements are timer trigged at set interval for example 5 s see setup.
if(timerTrig==TRUE){
duration = 0;
while(duration == 0){
duration = pulseIn(CO2_PIN, HIGH, 1004000);
}
cnt++;
meas++;
shortCnt++;
//duration = pulseIn(CO2_PIN, HIGH, 1004000);
if ( duration != 0) {
duration = ((duration -2000)*5)/1000;
//if(shortCnt>=5){
if(tmpPause==FALSE){
Serial.println(duration);
}
//shortCnt = 1;
}
u8g2.clearBuffer(); // Clear the internal memory
u8g2.setFont(u8g2_font_ncenB08_tr); // Choose a suitable font
//u8g2.drawStr(0, 10, "LASSE"); // Write text to x=0, y=10
u8g2.setCursor(0,30); // set cursor position
u8g2.print(duration);
u8g2.drawStr(0, 10, "CO2 ppm"); // Write text to x=0, y=30
u8g2.sendBuffer(); // Transfer internal memory to the display
// Log data interval in seconds is 5* limit below.
// Limit=12 --> interval 60 s. 180 --> 15 minutes.
if (((cnt>=180) || (cnt<1)) && (tmpPause==FALSE)){
// Convert time to seconds since start. %s measurement interval
line = String(meas*5)+String(" , ")+String(duration);
File dataFile = SD.open("/log.txt", FILE_APPEND);
if (dataFile) {
//Serial.println("File opened");
//dataFile.println(duration);
dataFile.println(line);
#ifdef TEST
Serial.print("Wrote:");
Serial.println(line);
#endif
dataFile.close();
//Serial.println("File closed");
} else {
Serial.println("File append failed!");
}
cnt = 1;
}
timerTrig = FALSE;
}
// Check for commands over the USB serial the main loop runs at 1/10 of a second
// Measurements are done at 5 s intervals controlled by the timer.
// CO2 is displayed at every measurtement but storage is done
if(Serial.available()){
String inputLine = Serial.readStringUntil('\n'); // Read until newline
inputLine.trim();
inputLine.toUpperCase();
#ifdef TEST
Serial.println(inputLine);
#endif
if(inputLine.substring(0,3)=="HEL"){
Serial.println();
Serial.println("\nCommands:");
Serial.println("HELP print a list of known kommands.");
Serial.println("LOG print the contents of the log file.");
Serial.println("CLEAR erase the log file and generate a new one.");
Serial.println("START Comment including start of measurement.");
Serial.println("PAUSE pause measurements to dump file to USB serial.");
Serial.println(" In PAUSE mode it is safe to remove the SD memory.");
Serial.println("CONTINUE contyinue measurements after PAUSE.");
}
if(inputLine.substring(0,3)=="CLE"){
#ifdef TEST
Serial.println("Command CLEAR");
#endif
deleteFile(SD,"/log.txt");
// Generate a new empty file
writeFile(SD,"/log.txt","\n");
}
if(inputLine.substring(0,3)=="LOG"){
#ifdef TEST
Serial.println("Command LOG");
#endif
tmpPause = TRUE;
readFile(SD, "/log.txt");
tmpPause = FALSE;
#ifdef TEST
Serial.println("Printed the log file to USB serial");
#endif
Serial.println();
}
if(inputLine.substring(0,3)=="PAU"){
#ifdef TEST
Serial.println("Command PAUSE");
#endif
tmpPause = TRUE;
}
if(inputLine.substring(0,3)=="CON"){
#ifdef TEST
Serial.println("Command CONTINUE");
#endif
tmpPause = FALSE;
}
if(inputLine.substring(0,3) == "STA"){
#ifdef TEST
Serial.println("Command START");
#endif
appendFile(SD,"/log.txt",inputLine.c_str());
appendFile(SD,"/log.txt","\n");
tmpPause = FALSE; // Ensure that measurements start regardless of history.
}
inputLine = "";
}
delay(100);
digitalWrite(LED_BUILTIN, LOW);
}

Trevlig WiFi kamera

18/11/2020

En digitalkamera, t.o.m. en högkvalitativ sådan kostar idag ingenting eftersom man tillverkar någon miljard per år för montering i främst mobiltelefoner. Eftersom kameror massproduceras så kommer det alltid att finnas ett överskott som bl.a. kan användas av tekniskt intresserade amatörer.

Jag stötte för en tid sedan på ESP32-CAM som bygger på en ESP32 mikroprocessor med kameramodulen OV2640. Hela paketet kostar $6.99 och inklusive transport kostade paketet ungefär 10 Euro (100 SEK).

ESP32-kortet har storleken ca. 40×25 mm. Trådarna som är kopplade till kortet är strömförsörjning via USB samt serielinje för att kunna följa med vad som händer på kortet. Serieförbindelsen behövs inte i ett senare skede och strömförsörjningen kommer att skötas med laddningsbara batterier.

Vad innehåller paketet?

Processorn är en ESP32 med dubbla kärnor, 512 kByte SRAM och 4 MByte pseudostatiskt RAM. Processorn kör på en klockfrekvens upp till 240 MHz och har alla vanliga anslutningar för periferienheter SPI, I2C, serielinje etc. samt inbyggd WiFi alltså trådlöst nätverk utan extra komponenter. En enkel utvecklingsomgivning som finns för Linux, Windows och MacOS är Arduino IDE. För att skriva program för ESP32 behövs en IDE version som är någorlunda ny, jag kör 1.8.13.

Hur lägger jag in webbservern för kameran om den inte finns från början?

Hämta Arduino IDE för ditt operativsystem. Googla ”Arduino IDE xxxxc” där xxxx är ditt operativsystem.

Starta Arduino IDE och lägg till kortfamiljen ESP32 via File/Preferences

Nere i fönstret finns ett fält för Additional Boards Manager URLs .

Klistra in https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json

Tryck på OK.

Gå därefter till Tools/Board/Board Manager . Skriv ESP32 i fältet Filter your Search. Board managern visar vad den hittar (esp32) klicka på Install varvid systemet automatiskt laddar ner de hjälpprogram, drivrutiner, bibliotek och exempel man behöver. Om man tidigare har laddat ner hjälprutiner för ESP32 så meddelar Board manager status: Installed för paketet.

Hämta kameraprogrammet i File/Examples/ESP32/Camera/CameraWebServer . Programkoden öppnas automatiskt i fönstret.

För att programmera ESP32 behöver jag en USB till Serial konversionsmodul som kostar några Euro. Moderna datorer saknar en konventionell serieport varför det behövs en USB-modul som skapar en serieport via USB-porten. Jag beställde en ”USB To TTL FT232RL FTDI Serial Adapter Converter Module For Arduino 3.3V 5V Mini” tillsammans med ESP32-CAM modulen.

Spänningen i USB seriemodulen väljs via en jumper (bygel) till antingen 3.3 V eller 5V. Väljer man 3.3V måste stiftet VCC på USB modulen kopplas till 3.3V på ESP32. Om man väljer 5V kopplas VCC i stället till 5V på ESP32. Det är viktigt att inte klanta och koppla 5V till 3.3V på ESP32 eftersom detta kan leda till att man släpper ut rök. Som känt fungerar all elektronik på rök eftersom elektronik tenderar att sluta fungera om man släpper ut röken.

USB-modulens RX kopplas till UOT på ESP32 och på motsvarande sätt kopplas TX på USB-modulen till UOR på ESP32. Notera att Transmit (sänd) på ena sidan alltid kopplas till Receive (mottag) på andra sidan och tvärtom.

Koppla GND på USB-modulen till GND på ESP32.

För programmering kopplas ytterligare IO0 till GND på ESP32. Byglingen av IO0 till jord signalerar Arduino IDE att uppladdning av program till ESP32 önskas. Då man vill köra ett uppladdat program kopplar man bort denna bygel.

Koppla i USB till datorn där Arduino IDE är aktivt. Kontrollera i Tools/Port att en serieport t.ex. /dev/ttyUSB0 under Linux har detekterats. Kontrollera samtidigt att Upload speed är satt till t.ex. 9212000 bit/sekund (kör man på lägre hastighet kan uppladdningen bli besvärande långsam). Sätt Tools/Partitition Scheme är satt till Huge APP . Glömmer man att organisera minnet till Huge App så kommer kompileringen att misslyckas.

Vi kan nu försöka kompilera exempelprogrammet via Sketch/Verify/Compile. Kompileringen gick inte igenom för mig vid första försöket eftersom en pythonmodul <serial> inte hittades. Det gick att identifiera problemet genom att felet låg i ett program med typen .py medan språket som används under Arduino IDE normalt är C/C++.Notera att felet inte låg i ESP32 vebbserverprogrammet utan det var ett hjälpprogram från ESP32 som behövde modulen. Felet avhjälptes genom att ladda in serial:

sudo apt install python-serial

Modifiera därefter programkoden så att du lägger in WiFi SSID samt password. Dessutom måste man välja kameramodell. I mitt fall fungerar alternativet:

#define CAMERA_MODEL_AI_THINKER

Man väljer kameramodell genom att ta bort kommentaren före ifrågavarande #define. En kommentar börjar med ”//”.

Nu gick kompileringen igenom och programmet kan laddas upp till ESP32. Tryck på Reset på ESP32, en mycket liten trycktangent bredvid 3.3V anslutningen. Välj nu Sketch/Upload och om allt går korrekt så börjar programmet laddas upp (tid kanske 30-60 sekunder beroende på vilken uppladdningshastighet man valt). Då uppladdningen har lyckats kopplar man ur programmeringsbygeln IO0-GND för att köra programmet.

Starta Tools/serial Monitoroch kontrollera att hastigheten är vettig t.ex. 115200. Tryck på ESP32 Reset varefter ESP bör skriva diverse text till monitorn. I texten hittar vi den IP adress som ESP32 har fått via det lokala WiFi nätverket.

Vi öppnar nu en vebbläsare (jag använder Firefox) och lägger in den IP-adress vi fick oss tilldelad och som vi grävde fram åt oss från texten på monitorskärmen. I mitt fall fick jag adressen http://192.168.10.42/. Notera att den angivna adressen ovan inte är verklig och den kommer inte att fungera i ditt fall.

Ett fönster med kamerakontroller på vänster sida öppnas i vebbläsaren. Längst ner finns kontrollerna Get still, Start stream . Tryck på Start Stream varvid ESP32 börjar sända video över WiFi till datorn. Det är nu möjligt att via kontrollerna ändra ljushet/mörkhet, kontrast, upplösning etc. Fritt fram att experimentera.

Följande steg?

Följande steg blir att planera och skriva ut ett lämpligt skal samt förse ESP32 med ett laddningsbart batteripaket så att jag kan hänga systemet på fågelbordet och avslöja våra stora (vitsvanshjortar 😉 ) småfåglar som länsar fågelbordet på nolltid.

Källor:

Det finns en hel del artiklar på engelska om hur ESP32 skall kopplas för programutveckling. Nedan enast ett exempel. Vid problem lönar det sig oftast att Google på den felkod man får. Det finns oftast någon annan som har stött på samma problem och en lösning kan ofta hittas direkt.

Miniprojekt: Metronom i klockan

02/09/2020

Jag har köpt en del småsaker, främst elektronik, från Banggood som är ett kinesiskt företag. Hittills har beställningarna kommit ungefär enligt beräknad tid men COVID-19 ställer till med problem. Tidigare beställningar

 
  • Visningar
  • Besökare

leder till att Banggood via facebook synbarligen aktiverar reklam aktuell för mig.

Jag råkade stöta på reklam för en ny aktivitetsklocka Lilygo T-Watch-2020. Klockan är baserad på en ESP32 mikroprocessor med två kärnor. Klockan innehåller WiFi funktionalitet med bl.a. en webbserver och också Bluetooth funktionalitet. Det speciella med den här klockan är att den från början är avsedd att hackas d.v.s. källkoden finns på github och den programmeras direkt över USB-anslutningen.

Då man får en ny programmeringsleksak så uppstår naturligtvis genast problemet med vad man skall göra med apparaten.  Färdiga funktioner är bl. a.

  • Väderapp
  • Stoppklocka
  • Kryptovaluta
  • Navigering via kart-API

Då jag sysslar med folkmusik som hobby så slog det mig att det kunde vara praktiskt att ha en enkel metronom i klockan som innan jag börjar spela ett stycke slår t.ex. tio slag med hjälp av klockans vibrationsmotor. Tanken är alltså inte att jag skulle spela hela stycken med klockan som metronom utan klockan skulle endast upplysa mig om ungefär vilket tempot är, att mentalt tänka sig ett tempo kan ibland leda fel rejält eftersom hjärnan inte i alla sammanhang går i realtid …

Operativsystem

Klockan kör ett kompakt realtidsoperativsystem FreeRTOS som stöder multitasking d.v.s. jag kan köra flera applikationer parallellt. Det är också i princip möjligt att ladda in nya program medan klockan är i gång eller stänga av obehövliga program. Den senare funktionaliteten används inte utan gör jag förändringar så kompilerar jag om rubbet och laddar upp allt som ett paket till klockan.

Man hittar FreeRTOS dokumentation på nätet.

Metronom

Jag plockade hem en klockversion från Github. Då jag ögnade igenom applikationerna som fanns färdigt så såg jag att det fanns en exempel app som inte gjorde någonting men som visade vilken struktur en app har.

Jag beslöt att försöka göra en extremt enkel applikation där användaren använder sig av en slider d.v.s. en funktion där man drar en indikator i sidled för att ställa in tempot. Jag tänkte mig att tempot kan ställas mellan 50 och 200 slag per minut. Det finns inga tekniska problem med att använda lägre tempon men jag uppfattar 50 slg/minut som väldigt långsamt. Då klockan startar är det förinställda värdet 100 slag/minut och klockan kommer ihåg det senast valda värdet mellan olika körningar av appen.

I princip borde applikationen ha varit väldigt enkel att realisera men det visade sig att indikatorhuvudet som samtidigt användes för att visa valt tempo var alltför litet d.v.s. det var svårt att se det valda tempot. Jag försökte kringgå problemet genom att lägga till ett separat textfält som skulle innehålla det valda värdet. Det nya textfältet förorsakade en omedelbar crasch.

Efter att jag två dagar hade slagit huvudet i väggen beslöt jag att byta ut slidern mot en spinbox där man i mitten har ett textfält och på vardera sidan tryckknappar som sänker/höjer det valda värdet. Det var tydligen något problem med slidern för nu fungerade val av tempo som det skulle.

Jag modifierade setupdelen i exempelapplikationen så att man väljer tempot med spinboxen och då man går ur tempofunktionen så gör klockan tio slag i det valda tempot varefter det sker retur till appens huvudnivå.

Call back funktioner

static void exit_example_app_setup_event_cb( lv_obj_t * obj, lv_event_t event );
static void example_app_metr_spinbox_event_cb(lv_obj_t * obj, lv_event_t event );
static void lv_spinbox_increment_event_cb(lv_obj_t * obj, lv_event_t event );
static void lv_spinbox_decrement_event_cb(lv_obj_t * obj, lv_event_t event );

Då användaren trycker på olika funktioner måste användargränssnittet kunna ”skicka signaler” till användarens kod så att den kan köras. För detta ändamål behöver vi såkallade callback funktioner. Om jag t.ex. trycker på knappen ”-” så stegas det valda tempot ner med ett steg (systemet anropar lv_spinbox_decrement_event_cb). Trycker jag på ”+” så stegas värdet upp med ett steg( lv_spinbox_increment_event_cb anropas).

Då jag trycker på återgång till appens huvudnivå så anropas exit_example_app_setup_event_cb. Denna funktion anropar funktionen execute_taps() som vibrerar telefonen tio gånger i önskat tempo.

// Let the metronome do ten taps
static void execute_taps(int taps, int tempo){
  int i;
  int taplen=0;
  int fixed = 100;
  float ms=60000;
  taplen = ms/tempo; 
  // Buzzer pin output BUZZER=4
  for(i=0; i< taps; i++){ 
    pinMode(BUZZER,OUTPUT);
    digitalWrite(BUZZER,ON);
    delay(fixed);
    digitalWrite(BUZZER,OFF);
    delay(taplen-fixed);
  }
}

Spinnboxen

Då klockan programmeras så har man tillgång till ett enkelt grafiskt användargränssnitt lvgl vilket i ytterst hög grad förenklar programmeringen. Problemet är dock att det finns rätt många olika grafiska gränssnitt och det kräver alltid en del läsande att börja använda ett nytt gränssnitt … så även denna gång.

Koden för att sätta upp en spinbox är:

// Create a spinbox center box with value
lv_obj_t *example_app_metr_spinbox_cont = lv_obj_create(example_app_setup_tile,NULL);
lv_obj_set_pos(example_app_metr_spinbox_cont,90,100);
example_app_metr_spinbox = lv_spinbox_create(example_app_metr_spinbox_cont, NULL ); 
lv_spinbox_set_range(example_app_metr_spinbox,50,200);
lv_spinbox_set_digit_format(example_app_metr_spinbox,3,0);
lv_spinbox_set_value(example_app_metr_spinbox,100);
lv_obj_set_event_cb(example_app_metr_spinbox,example_app_metr_spinbox_event_cb);
// Create increment button
lv_coord_t h = lv_obj_get_height(example_app_metr_spinbox);
lv_obj_t * btn = lv_btn_create(example_app_setup_tile, NULL);
lv_obj_set_size(btn, h, h);
lv_theme_apply(btn, LV_THEME_SPINBOX_BTN);
lv_obj_set_style_local_value_str(btn, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_SYMBOL_PLUS);
lv_obj_align(btn, example_app_metr_spinbox, LV_ALIGN_OUT_RIGHT_MID, -20, 0);
lv_theme_apply(btn, LV_THEME_SPINBOX_BTN);
lv_obj_set_style_local_value_str(btn, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_SYMBOL_PLUS);
lv_obj_set_event_cb(btn, lv_spinbox_increment_event_cb);
// Create decrement button
btn = lv_btn_create(example_app_setup_tile, btn);
lv_obj_align(btn, example_app_metr_spinbox, LV_ALIGN_OUT_LEFT_MID, -30, 0);
lv_obj_set_event_cb(btn, lv_spinbox_decrement_event_cb);
lv_obj_set_style_local_value_str(btn, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_SYMBOL_MINUS);

Klockans skärm har upplösningen 240×240 pixlar. Först skapas ett kontext för spinboxen i filen example_app_setup.cpp utgående från specifikationer i appens huvudfönster. Därefter placeras spinnboxen på önskat ställe och programmet ställer in hurudant värdeområde (50-200 slag/minut) som är tillåtna. Programmet ställer också in vilket standardvärde (100 slag/minut) jag vill ha. Därefter skapar jag två tangenter/knappar som stegar spinboxens varde uppåt respektive nedåt.

Grafiska änvändargränssnitt fungerar så att skapade vidgetar själv detekterar om det händer något. I ovanstående fall så kommer tangenterna upp/ner att skicka en ”signal” som säger att en knapptryckning har detekterats. För att programmet skall kunna reagera på knapptryckningen så måste vi definiera såkallade callback funktioner d.v.s. funktioner som användargränssnittet anropar då aktivitet detekteras. I vårt fall är funktionen enkel:

static void lv_spinbox_increment_event_cb(lv_obj_t * obj, lv_event_t event ){
  if(event == LV_EVENT_SHORT_CLICKED || event == LV_EVENT_LONG_PRESSED_REPEAT) {
    lv_spinbox_increment(example_app_metr_spinbox);
  }
}

static void lv_spinbox_decrement_event_cb(lv_obj_t * obj, lv_event_t event ){
  if(event == LV_EVENT_SHORT_CLICKED || event == LV_EVENT_LONG_PRESSED_REPEAT) {
    lv_spinbox_decrement(example_app_metr_spinbox);
  }
}

När spinnboxens kontrollknappar detekterar aktivitet så anropas ifrågavarande funktioner ovan som i sin tur stegar upp spinboxens värde eller stegar ner det.

Aktivera metronomen

Jag valde att aktivera metronomen då man går ut ur inställningen av tempo tillbaka till appens huvudsida. Alternativet skulle ha varit att definiera en separat knapp för att aktivera metronomen. Jag har valt att begränsa antalet slag till tio helt enkelt för att jag uppfattar att jag då har hunnit uppfatta tempot tillräckligt bra.

static void exit_example_app_setup_event_cb( lv_obj_t * obj, lv_event_t event ) {
  switch( event ) {
    case( LV_EVENT_CLICKED ): 
      delay(2000);
      execute_taps(10,lv_spinbox_get_value(example_app_metr_spinbox));
      mainbar_jump_to_tilenumber( example_app_get_app_main_tile_num(), LV_ANIM_ON );
      break;
  }
}

Koden för execute_taps() finns litad tidigare i texten.

Att använda appen

IMGP7532

Fig. 1  Metronomappen syns till höger om väderappen nedanför tiden 20:57.

IMGP7533

Fig. 2  Applikationen metronom (Metr) huvudsida efter att man har valt applikationen genom att trycka på Metr i föregående bild. Kugghjulet leder till val av tempo. I framtiden lägger jag antagligen till t.ex. fyra knappar med fördefinierade tempon på denna sida.

IMGP7534

Fig. 3  Inställning av tempot med en spinbox. Det valda tempot stannar i minnet. Då man väljer retur (trycker på symbolen överst till vänster) så vibrerar telefonen tio gånger i det angivna tempot.

 


Pointman's

A lagrange point in life

THE HOCKEY SCHTICK

Lars Silén: Reflex och Spegling

NoTricksZone

Lars Silén: Reflex och Spegling

Big Picture News, Informed Analysis

Canadian journalist Donna Laframboise. Former National Post & Toronto Star columnist, past vice president of the Canadian Civil Liberties Association.

JoNova

Lars Silén: Reflex och Spegling

Climate Audit

by Steve McIntyre

Musings from the Chiefio

Techno bits and mind pleasers

Bishop Hill

Lars Silén: Reflex och Spegling

Watts Up With That?

The world's most viewed site on global warming and climate change

TED Blog

The TED Blog shares news about TED Talks and TED Conferences.

Larsil2009's Blog

Lars Silén: Reflex och Spegling