Part 1 — The Walkthrough
LGL is a lightweight LivinGrimoire port for the Arduino microcontroller — a modular, plug-and-play skill system for Arduino.
This walkthrough covers how to add LGL skills to an Arduino, and how to talk to them using LivinGrimoire on Python.
The Python side is the brain. The Arduino side is the cerebellum — it handles the reflexes: sensors, actuators, the physical stuff, while Python does the thinking and talks back to it over serial.
Hello World
The Hello World skill blinks the default onboard LED (pin 13) twice, then stops. It exists to prove the Arduino ↔ LivinGrimoireLight wiring works before building anything real on top of it.
Step 1 — Install the LivinGrimoireLight library
In the Arduino IDE:
- Tools → select your board and the port your Arduino/Elegoo is connected to.
- Sketch → Include Library → Add .ZIP Library...
- Select
LivinGrimoireLight290724.zip.
Library source: LivinGrimoireLight (GitHub)
Step 2 — Write the main sketch
No extra .h/.cpp files are needed for Hello World — it uses the built-in DiHelloWorld skill that ships with the library. Open your sketch's main .ino file and paste this in:
// the code will blink twice the default Led, Led #13
#include "DiHelloWorld.h"
#include "LivinGrimoireLight.h"
void setup() {
Led led1(13); // used to initialize the Hello World skill
Skill* s2 = new DiHelloWorld(led1); // example skill created
Chobit* c1 = new Chobit();
c1->addSkill(s2);
c1->think(0, 0, 0);
c1->think(0, 0, 0); // called twice -> LED blinks twice
}
void loop() {
}
Step 3 — Upload and verify
Upload the sketch. The onboard LED (pin 13) should blink twice and then stop — think() is only called twice inside setup(), and loop() is left empty on purpose.
That's the whole pattern: create a Led/pin → wrap it in a Skill → add the Skill to a Chobit → call think(). Everything else below reuses this exact pattern on the Arduino side, then adds the laptop side.
Temperature + Blinker — the lazy way
These two skills are the ones people actually use together: read a temperature, blink a light. Here's the real setup — one Arduino sketch with both skills, and one laptop file that self-registers both skills. Follow the steps in order.
Step 1 — Add the Temperature skill files
In the Arduino IDE, open a new tab (top-right arrow → New Tab) and name it DiTemperature.h. Paste this in:
#ifndef DiTemperature_H
#define DiTemperature_H
// using Arduino hardware codes outside main:
#include <Arduino.h>
#include "LivinGrimoireLight.h"
/*
lm35 sketch
prints the temperature to the serial monitor
*/
class DiTemperature : public Skill {
private:
int _inPin; // analog pin
public:
DiTemperature(int inPin);
virtual void inOut(byte ear, byte skin, byte eye);
};
#endif
Open a second new tab, name it DiTemperature.cpp, and paste this in:
#include <Arduino.h>
#include "DiTemperature.h"
#include "LivinGrimoireLight.h"
DiTemperature::DiTemperature(int inPin)
{
_inPin = inPin;
Serial.begin(9600); // Start serial communication at 9600 baud
}
void DiTemperature::inOut(byte ear, byte skin, byte eye) {
int value = analogRead(_inPin);
float millivolts = (value / 1024.0) * 5000;
float celsius = millivolts / 10;
Serial.print(celsius);
delay(1000);
// Serial.print(" \xC2\xB0"); // shows degree symbol
// Serial.println("C");
}
This skill reads the LM35 sensor and prints the Celsius value over serial. Wire the LM35's signal pin to analog pin A0 on the board — that's what pin 0 in the constructor refers to.
Step 2 — Add the Blinker skill files
Open a third tab, name it DiBlinker.h, and paste this in:
#ifndef DiBlinker_H
#define DiBlinker_H
// using Arduino hardware codes outside main:
#include <Arduino.h>
#include "LivinGrimoireLight.h"
#include "DiHelloWorld.h"
// example hello world by blinking default Led #13 once
class DiBlinker : public Skill {
private:
Led _l1;
public:
DiBlinker(Led l1);
virtual void inOut(byte ear, byte skin, byte eye);
};
#endif
Open a fourth tab, name it DiBlinker.cpp, and paste this in:
#include <Arduino.h>
#include "DiBlinker.h"
#include "LivinGrimoireLight.h"
#include "DiHelloWorld.h"
DiBlinker::DiBlinker(Led l1)
{
Serial.begin(9600);
_l1 = l1;
_l1.init();
}
void DiBlinker::inOut(byte ear, byte skin, byte eye)
{
// Check if data is available to read
if (Serial.available() > 0) {
// Read the incoming byte
char data = Serial.read();
// Blink the LED if the received data is '1'
if (data == '1') {
_l1.on();
delay(1000);
_l1.off();
delay(1000);
}
}
}
This skill listens on serial for the byte '1'. When it arrives, it blinks the LED on pin 13 once.
Step 3 — Wire both skills into the main sketch
Go back to your main .ino tab and replace its contents with this — it creates both skills and adds them to the same Chobit:
#include "DiTemperature.h"
#include "DiBlinker.h"
#include "LivinGrimoireLight.h"
Chobit* c1;
void setup() {
Led led1(13);
Skill* temp = new DiTemperature(0); // LM35 on analog pin A0
Skill* blink = new DiBlinker(led1); // LED on pin 13
c1 = new Chobit();
c1->addSkill(temp);
c1->addSkill(blink);
}
void loop() {
c1->think(0, 0, 0);
}
Upload it. That's the whole Arduino side — done once, never touched again.
Step 4 — Install pyserial
On the laptop, open your PyCharm terminal and run:
pip install pyserial
Step 5 — Drop in the DLC file
Create a file named DLC_arduino.py in your Python project directory and paste this in:
import serial
import time
import atexit
import serial.tools.list_ports
from LivinGrimoirePacket.AXPython import RegexUtil
from LivinGrimoirePacket.LivinGrimoire import Skill, Brain
# terminal: pip install pyserial
def is_port_available(param):
ports = serial.tools.list_ports.comports()
for port in ports:
if port.device == param:
return True
return False
name_of_port = 'COM3'
if is_port_available(name_of_port):
ser = serial.Serial(name_of_port, 9600, timeout=0.1)
print("Arduino connected successfully.")
else:
ser = None
print("Arduino not connected. Please check the connection.")
def close():
if ser:
ser.close()
class SerialReader:
@staticmethod
def read_serial_data(num_readings=10) -> str:
try:
for _ in range(num_readings):
if ser.in_waiting > 0:
line = ser.readline().decode('utf-8').rstrip()
line = RegexUtil.extractRegex("[-+]?[0-9]{1,3}", line)
if len(line) > 0 and line != "0":
return f'{int(line) - 10}'
time.sleep(1) # Delay between readings
except serial.SerialException as e:
return f"Error reading serial data: {e}"
return "i do not know"
class DiArduinoTemperature(Skill):
# example skill for reading Arduino data
def __init__(self):
super().__init__()
# Override
def input(self, ear: str, skin: str, eye: str):
if ear == "check temperature":
self.setVerbatimAlg(4, SerialReader.read_serial_data())
class DiBlinker(Skill):
# blinks the Arduino led, this is an example of sending commands to the Arduino
def __init__(self):
super().__init__()
# Override
def input(self, ear: str, skin: str, eye: str):
if ear == "blink":
self.setVerbatimAlg(4, "blinking")
ser.write(b'1')
def add_DLC_skills(brain: Brain):
if ser is None:
return
atexit.register(close) # wrap up serial object when program closes
brain.add_logical_skill(DiArduinoTemperature())
brain.add_logical_skill(DiBlinker())
Change name_of_port = 'COM3' to match your actual port if it's different. Because the filename contains DLC, the auto-loader (call_add_DLC_skills(brain) in your main file) finds this file and registers both skills automatically — you don't call add_logical_skill(...) yourself anywhere.
What it's doing, in plain terms:
- On import, it looks for the Arduino on the given port and opens the serial connection. If it's not found,
serstaysNoneandadd_DLC_skillsquietly skips registering anything — no crash if the Arduino isn't plugged in. -
SerialReader.read_serial_data()reads lines off the serial port, pulls the number out with a regex, and subtracts10as a calibration correction before returning it as a string. -
DiArduinoTemperatureonly fires when the ear input is exactly"check temperature"— it grabs a reading and hands it back throughsetVerbatimAlg. -
DiBlinkeronly fires on"blink"— it writes a single'1'byte to the Arduino, which is the same byteDiBlinker.cppon the Arduino side is listening for. -
add_DLC_skills(brain)is the entry point the auto-loader calls — it registers both skills on the brain and makes sure the serial port gets closed cleanly withatexit.
Step 6 — Use it
Once it's loaded, you don't call any serial code yourself — you just talk to the brain:
brain.input("check temperature", "", "")
brain.input("blink", "", "")
Say "check temperature" and you get the LM35 reading back. Say "blink" and the Arduino's LED blinks once. Everything in between — port handling, regex parsing, the byte-write — is already done for you inside the DLC file.
Recap
-
Arduino side: each skill is a
.h/.cpppair implementingSkill; add each one tosetup()withaddSkill(...). -
Laptop side: each skill (or group of skills, like here) lives in one
..._DLC...pyfile with anadd_DLC_skills(brain)function; the auto-loader finds and registers it — no manual wiring. -
Using it: call
brain.input("<trigger phrase>", "", ""). Whatever the skill'sinput()matches on, that's your trigger.
Part 2 — The Skill Crafter (let an LLM write the next skill for you)
This is part of the effort to democratize AI: you don't need to be a programmer to give an Arduino a real-world skill anymore. You describe what you want in plain English, and an LLM writes the actual code for you — no LGL knowledge required on your end, and none required from the LLM either, since the prompt teaches it everything as it goes.
A quick reminder of the two sides this generates code for: the Python side is the brain — it decides what to do. The Arduino side is the robotics side — the part that actually touches the real world, wired directly to physical sensors and motors, carrying out what the brain tells it and reporting back.
Feed this prompt a plain-English description of a skill, and it hands back three ready-to-paste files: the Arduino .h, the Arduino .cpp, and a standalone Python DLC file.
How to use
1. Copy the prompt. Copy everything inside the big code box below.
2. Fill in your skill. Replace {description goes here} on the first line with what you want the skill to do — as plain and specific as you like, e.g. "a skill that turns on a fan when a temperature sensor reads above 30°C."
3. Run it. Paste the whole thing into an LLM. It will hand back three code blocks: <DiSkillName>.h, <DiSkillName>.cpp, and DLC_<topic>.py.
4. Install the LivinGrimoireLight library (only needs to be done once, ever — skip this step if you've already got it installed). In the Arduino IDE:
- Tools → select your board and the port your Arduino/Elegoo is connected to.
- Sketch → Include Library → Add .ZIP Library...
- Select
LivinGrimoireLight290724.zip.
5. Add the .h and .cpp files to your Arduino sketch. In the Arduino IDE, open a new tab (the small arrow in the top-right of the editor → New Tab), name it exactly <DiSkillName>.h, and paste in the generated header code. Open another new tab named <DiSkillName>.cpp and paste in the generated implementation code. Do this once per file the LLM gave you — a sketch can hold as many skill tabs as you like.
6. Wire the skill into your main sketch. Having the .h/.cpp files in the sketch isn't enough on its own — your main .ino file also has to create the skill and register it with a Chobit so it actually runs. Here's a real example wiring up two skills, DiTemperature and DiBlinker, to show the pattern:
#include "DiTemperature.h"
#include "DiBlinker.h"
#include "LivinGrimoireLight.h"
Chobit* c1;
void setup() {
Led led1(13);
Skill* temp = new DiTemperature(0); // LM35 on analog pin A0
Skill* blink = new DiBlinker(led1); // LED on pin 13
c1 = new Chobit();
c1->addSkill(temp); // registers the temperature skill
c1->addSkill(blink); // registers the blinker skill
}
void loop() {
c1->think(0, 0, 0);
}
Every skill follows this same shape: create it with new <DiSkillName>(...), pass it whatever hardware it needs (a pin number, a Led object, etc. — matches the constructor the LLM generated), and register it with c1->addSkill(...). c1->think(0, 0, 0) in loop() is what actually ticks every registered skill, so it only needs to be called once no matter how many skills you've added.
7. Drop the Python DLC file into your project. Save the generated DLC_<topic>.py file into the DLC directory of your LivinGrimoire project — no extra wiring needed there, the auto-loader picks it up automatically because "DLC" is in the filename.
The Prompt
SKILL TO GENERATE: {description goes here}
────────────────────────────────────
You are generating code for LivinGrimoireLight (LGL), a modular skill framework pairing an Arduino microcontroller with a Python "brain" on a laptop over serial. You have never seen this framework before. Everything you need is below. Follow it exactly, even where it looks unusual — it's load-bearing for the rest of the codebase, and it applies regardless of what kind of skill you're building (sensor, actuator, timer, protocol handler, anything).
WHAT LGL IS
No central orchestrator. Every capability is an independent "Skill" object owning its own trigger and its own behavior. Two synced halves: Arduino side does raw hardware I/O, Python side does phrase-dispatch and can command the Arduino back over serial.
HARD NAMING RULE
Every skill class, in both C++ and Python, is named with a "Di" prefix — DiWhateverThisSkillDoes. File names match the class name. The Python DLC file is named DLC_<lowercase topic>.py, separate from the class name(s) inside it.
ARDUINO SIDE
- `Skill` is the base class (from LivinGrimoireLight.h, already provided — do not redefine it).
- Every skill implements this exact signature, unchanged even if a parameter goes unused:
virtual void inOut(byte ear, byte skin, byte eye);
ear/skin/eye are generic input channels; simple hardware skills usually ignore the passed values (caller sends 0,0,0) and read real pins directly inside inOut().
- `Chobit` drives skills: `chobit->addSkill(skillPtr)` registers one, `chobit->think(0,0,0)` calls `inOut()` on every registered skill for one tick — typically called every `loop()` cycle.
- One `.h`/`.cpp` pair per skill. `.h`: include guards, `#include <Arduino.h>` + `#include "LivinGrimoireLight.h"` (+ `"DiHelloWorld.h"` only if a `Led` object is needed), class extending `Skill`, private hardware-state members, constructor declaration, the `inOut` override declaration. `.cpp`: constructor assigns members (`Serial.begin(9600)` only if this skill owns the connection — state that assumption in a comment), `inOut()` holds the real logic.
- Avoid `delay()` over ~1000ms inside `inOut()` unless the description explicitly needs a blocking wait — it runs in a shared tick loop with every other skill on the same Chobit, so a long delay stalls all of them.
PYTHON SIDE
- `Skill` (`from LivinGrimoirePacket.LivinGrimoire import Skill, Brain`) is the Python mirror, triggered by phrase-matching instead of a hardware tick.
- Every skill implements this exact signature:
def input(self, ear: str, skin: str, eye: str):
Match a trigger phrase, e.g. `if ear == "some phrase":`.
- Output via `self.setVerbatimAlg(priority: int, response: str)` (priority 4 is standard). Command the Arduino by writing raw bytes to the open serial object.
- `Brain.add_logical_skill(skillInstance)` registers a skill; `brain.input(ear, skin, eye)` fires dispatch.
- A DLC file is a standalone `.py` file with "DLC" in its name. A project-level autoloader scans for these, imports them, and calls their `add_DLC_skills(brain)` — one file dropped in, zero manual registration elsewhere. A DLC file that talks to the Arduino sets up its own pyserial connection at module level; it never assumes an external connection already exists.
STRUCTURAL SKELETON (shape only — do not reuse this domain, it's illustrative)
<DiSkillName>.h:
#ifndef DiSkillName_H
#define DiSkillName_H
#include <Arduino.h>
#include "LivinGrimoireLight.h"
class DiSkillName : public Skill {
private:
// hardware state fields go here
public:
DiSkillName(/* constructor params matching the hardware described */);
virtual void inOut(byte ear, byte skin, byte eye);
};
#endif
<DiSkillName>.cpp:
#include <Arduino.h>
#include "DiSkillName.h"
#include "LivinGrimoireLight.h"
DiSkillName::DiSkillName(/* params */) {
// assign members, Serial.begin(9600) only if this skill owns serial
}
void DiSkillName::inOut(byte ear, byte skin, byte eye) {
// real sensor/actuator logic goes here
}
DLC_<topic>.py:
# pip install <whatever this skill actually needs>
import serial
import time
import atexit
import serial.tools.list_ports
from LivinGrimoirePacket.LivinGrimoire import Skill, Brain
def is_port_available(param):
ports = serial.tools.list_ports.comports()
for port in ports:
if port.device == param:
return True
return False
name_of_port = 'COM3'
if is_port_available(name_of_port):
ser = serial.Serial(name_of_port, 9600, timeout=0.1)
print("Arduino connected successfully.")
else:
ser = None
print("Arduino not connected. Please check the connection.")
def close():
if ser:
ser.close()
class DiSkillName(Skill):
def __init__(self):
super().__init__()
def input(self, ear: str, skin: str, eye: str):
if ear == "trigger phrase":
self.setVerbatimAlg(4, "response")
# ser.write(...) here if this skill commands the Arduino
def add_DLC_skills(brain: Brain):
if ser is None:
return
atexit.register(close)
brain.add_logical_skill(DiSkillName())
YOUR TASK
Using the skeleton's shape (not its content) and the skill description at the top of this prompt, generate three files:
1. <DiSkillName>.h — real class name starting with "Di", correct includes, real private state, real constructor signature, `virtual void inOut(byte ear, byte skin, byte eye);` unchanged.
2. <DiSkillName>.cpp — real constructor logic and real `inOut()` behavior. No blocking delay() over ~1000ms unless the description demands it. Comment non-obvious lines (unit conversions, magic numbers, protocol bytes).
3. DLC_<topic>.py — standalone file starting with a `# pip install ...` comment listing every real dependency, port-detection + safe `ser`/`close()` setup, one Di-prefixed Skill subclass per distinct behavior described (invent a short natural trigger phrase if none is given, note it as a comment), and `add_DLC_skills(brain)` guarded by `if ser is None: return`.
If anything is ambiguous (pins, phrases, thresholds, port), make the most reasonable assumption and note it as a one-line comment at point of use. Don't ask a clarifying question. Don't write anything outside the three code blocks — no summary, no preamble.
OUTPUT FORMAT
Exactly three fenced code blocks in this order, each preceded by a bolded file-name header:
**<DiSkillName>.h**
```cpp
...
```
**<DiSkillName>.cpp**
```cpp
...
```
**DLC_<topic>.py**
```python
...
```
Nothing else.
Become CoderPunk
Check it out at: yotamarker/LivinGrimoire: the LivinGrimoire software design pattern
Top comments (0)