Software apps and online services | ||||||
![]() |
|
The attached code is functionally the same as stock code, but it is MUCH more edit friendly. For example, here is the character data for `A
`:
const Character letters[] = {
{'A', (const uint8_t[]){ 0, 124, 140, 32, 112 }, 6},
...
}
First of all, the letter `A` is stored right next to the data it goes with. This makes it much easier to drop in your own character without setting off the entire alphabet. Second, the amount of numbers (`6`) is stored right at the end of the character data, so you can drop in ANY size you want, and adjust the number as necessary.
Because we're not buffering with 200's, this is saving like 1/2 the space that the letters used to take up.
This is a repost of my original post on the Hack Pack Discord
BetterDefault Code
ArduinoReplacememt code for https://ide.crunchlabs.com/editor/label-maker/3/stock.ino
//////////////////////////////////////////////////
// LIBRARIES //
//////////////////////////////////////////////////
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <Stepper.h>
#include <ezButton.h>
#include <Servo.h>
// Define the structure of our Character object
struct Character {
char letter;
const uint8_t* data;
uint8_t length;
};
/*
encoding works as follows:
ones = y coordinate;
tens = x coordinate;
hundreds = draw/don't draw ..
200 = end
222 = plot point
!! for some reason leading zeros cause problems !!
NOTE:
Include the length of the character at the very end of the data, as seen in the following.
*/
const Character letters[] = {
{'A', (const uint8_t[]){ 0, 124, 140, 32, 112 }, 6},
{'B', (const uint8_t[]){ 0, 104, 134, 132, 2, 142, 140, 100 }, 8},
{'C', (const uint8_t[]){ 41, 130, 110, 101, 103, 114, 134, 143 }, 8},
{'D', (const uint8_t[]){ 0, 104, 134, 143, 141, 130, 100 }, 7},
{'E', (const uint8_t[]){ 40, 100, 104, 144, 22, 102 }, 6},
{'F', (const uint8_t[]){ 0, 104, 144, 22, 102 }, 5},
{'G', (const uint8_t[]){ 44, 104, 100, 140, 142, 122 }, 6},
{'H', (const uint8_t[]){ 0, 104, 2, 142, 44, 140 }, 6},
{'I', (const uint8_t[]){ 0, 104 }, 2},
{'J', (const uint8_t[]){ 1, 110, 130, 141, 144 }, 5},
{'K', (const uint8_t[]){ 0, 104, 2, 142, 140, 22, 144 }, 7},
{'L', (const uint8_t[]){ 40, 100, 104 }, 3},
{'M', (const uint8_t[]){ 0, 104, 122, 144, 140 }, 5},
{'N', (const uint8_t[]){ 0, 104, 140, 144 }, 4},
{'O', (const uint8_t[]){ 10, 101, 103, 114, 134, 143, 141, 130, 110 }, 9},
{'P', (const uint8_t[]){ 0, 104, 144, 142, 102 }, 5},
{'Q', (const uint8_t[]){ 0, 104, 144, 142, 120, 100, 22, 140 }, 8},
{'R', (const uint8_t[]){ 0, 104, 144, 142, 102, 22, 140 }, 7},
{'S', (const uint8_t[]){ 0, 140, 142, 102, 104, 144 }, 6},
{'T', (const uint8_t[]){ 20, 124, 4, 144 }, 4},
{'U', (const uint8_t[]){ 4, 101, 110, 130, 141, 144 }, 6},
{'V', (const uint8_t[]){ 4, 120, 144 }, 3},
{'W', (const uint8_t[]){ 4, 100, 122, 140, 144 }, 5},
{'X', (const uint8_t[]){ 0, 144, 4, 140 }, 4},
{'Y', (const uint8_t[]){ 4, 122, 144, 22, 120 }, 5},
{'Z', (const uint8_t[]){ 4, 144, 100, 140 }, 4},
{'0', (const uint8_t[]){ 0, 104, 144, 140, 100, 144 }, 6},
{'1', (const uint8_t[]){ 0, 140, 20, 124, 104 }, 5},
{'2', (const uint8_t[]){ 4, 144, 142, 102, 100, 140 }, 6},
{'3', (const uint8_t[]){ 0, 140, 144, 104, 12, 142 }, 6},
{'4', (const uint8_t[]){ 20, 123, 42, 102, 104 }, 5},
{'5', (const uint8_t[]){ 0, 140, 142, 102, 104, 144 }, 6},
{'6', (const uint8_t[]){ 2, 142, 140, 100, 104, 144 }, 6},
{'7', (const uint8_t[]){ 0, 144, 104, 12, 132 }, 5},
{'8', (const uint8_t[]){ 0, 140, 144, 104, 100, 2, 142 }, 7},
{'9', (const uint8_t[]){ 0, 140, 144, 104, 102, 142 }, 6},
{'!', (const uint8_t[]){ 0, 222, 1, 104 }, 4},
{'?', (const uint8_t[]){ 20, 222, 21, 122, 142, 144, 104 }, 7},
{',', (const uint8_t[]){ 0, 111 }, 2},
{'.', (const uint8_t[]){ 0, 222 }, 2},
{'#', (const uint8_t[]){ 10, 114, 34, 130, 41, 101, 3, 143 }, 8},
{'@', (const uint8_t[]){ 31, 133, 113, 111, 141, 144, 104, 100, 140 }, 9}
};
// Alphabet is built later in setup, dynamically
String alphabet = "_";
int alphabetSize = 1;
const Character* getCharacter(char c) {
for (uint8_t i = 0; i < sizeof(letters) / sizeof(letters[0]); i++) {
if (letters[i].letter == c) {
return &letters[i];
}
}
return nullptr;
}
//////////////////////////////////////////////////
// PINS AND PARAMETERS //
//////////////////////////////////////////////////
LiquidCrystal_I2C lcd(0x27, 16, 2); // Set the LCD address to 0x27 for a 16x2 display
ezButton button1(14); //joystick button handler
#define INIT_MSG "Initializing..." // Text to display on startup
#define MODE_NAME " LABELMAKER " //these are variables for the text which is displayed in different menus.
#define PRINT_CONF " PRINT LABEL? " //try changing these, or making new ones and adding conditions for when they are used
#define PRINTING " PRINTING " // NOTE: this text must be 16 characters or LESS in order to fit on the screen correctly
#define MENU_CLEAR ": " //this one clears the menu for editing
//text variables
int x_scale = 230; //these are multiplied against the stored coordinate (between 0 and 4) to get the actual number of steps moved
int y_scale = 230; //for example, if this is 230(default), then 230(scale) x 4(max coordinate) = 920 (motor steps)
int scale = x_scale;
int space = x_scale * 5; //space size between letters (as steps) based on X scale in order to match letter width
//multiplied by 5 because the scale variables are multiplied against coordinates later, while space is just fed in directly, so it needs to be scaled up by 5 to match
// Joystick setup
const int joystickXPin = A2; // Connect the joystick X-axis to this analog pin
const int joystickYPin = A1; // Connect the joystick Y-axis to this analog pin
const int joystickButtonThreshold = 200; // Adjust this threshold value based on your joystick
// Menu parameters
String text; // Store the label text
int currentCharacter = 0; //keep track of which character is currently displayed under the cursor
int cursorPosition = 0; //keeps track of the cursor position (left to right) on the screen
int currentPage = 0; //keeps track of the current page for menus
const int charactersPerPage = 16; //number of characters that can fit on one row of the screen
// Stepper motor parameters
const int stepCount = 200;
const int stepsPerRevolution = 2048;
// initialize the stepper library for both steppers:
Stepper xStepper(stepsPerRevolution, 6, 8, 7, 9);
Stepper yStepper(stepsPerRevolution, 2, 4, 3, 5);
int xPins[4] = { 6, 8, 7, 9 }; // pins for x-motor coils
int yPins[4] = { 2, 4, 3, 5 }; // pins for y-motor coils
//Servo
const int SERVO_PIN = 13;
Servo servo;
int angle = 30; // the current angle of servo motor
// Creates states to store what the current menu and joystick states are
// Decoupling the state from other functions is good because it means the sensor / screen aren't hardcoded into every single action and can be handled at a higher level
enum State { MainMenu,
Editing,
PrintConfirmation,
Printing };
State currentState = MainMenu;
State prevState = Printing;
enum jState { LEFT,
RIGHT,
UP,
DOWN,
MIDDLE,
UPRIGHT,
UPLEFT,
DOWNRIGHT,
DOWNLEFT };
jState joyState = MIDDLE;
jState prevJoyState = MIDDLE;
boolean pPenOnPaper = false; // pen on paper in previous cycle
int lineCount = 0;
int xpos = 0;
int ypos = 0;
const int posS = 2;
const int posM = 7;
const int posL = 12;
bool joyUp;
bool joyDown;
bool joyLeft;
bool joyRight;
int button1State;
int joystickX;
int joystickY;
//////////////////////////////////////////////////
// S E T U P //
//////////////////////////////////////////////////
void setup() {
Serial.begin(9600);
// Build alphabet from chars we have
for (const Character& letter : letters) {
alphabet += letter.letter;
alphabetSize++;
}
Serial.println(alphabet);
Serial.println(alphabetSize);
lcd.init();
lcd.backlight();
lcd.setCursor(0, 0);
lcd.print(INIT_MSG); // print start up message
pinMode(LED_BUILTIN, OUTPUT);
button1.setDebounceTime(50); //debounce prevents the joystick button from triggering twice when clicked
servo.attach(SERVO_PIN); // attaches the servo on pin 9 to the servo object
servo.write(angle);
plot(false); //servo to tape surface so pen can be inserted
// set the speed of the motors
yStepper.setSpeed(12); // set first stepper speed (these should stay the same)
xStepper.setSpeed(10); // set second stepper speed (^ weird stuff happens when you push it too fast)
penUp(); //ensure that the servo is lifting the pen carriage away from the tape
homeYAxis(); //lower the Y axis all the way to the bottom
ypos = 0;
xpos = 0;
releaseMotors();
lcd.clear();
}
////////////////////////////////////////////////
// L O O P //
////////////////////////////////////////////////
void loop() {
button1.loop();
button1State = button1.getState();
joystickX = analogRead(joystickXPin);
joystickY = analogRead(joystickYPin);
joyUp = joystickY < (512 - joystickButtonThreshold);
joyDown = joystickY > (512 + joystickButtonThreshold);
joyLeft = joystickX < (512 - joystickButtonThreshold);
joyRight = joystickX > (512 + joystickButtonThreshold);
switch (currentState) { //state machine that determines what to do with the input controls based on what mode the device is in
case MainMenu:
{
if (prevState != MainMenu) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(MODE_NAME);
lcd.setCursor(0, 1);
lcd.print(" START ");
cursorPosition = 5;
prevState = MainMenu;
}
lcd.setCursor(cursorPosition, 1);
if (millis() % 600 < 400) { // Blink every 500 ms
lcd.print(">");
} else {
lcd.print(" ");
}
if (button1.isPressed()) { //handles clicking options in text size setting
lcd.clear();
currentState = Editing;
prevState = MainMenu;
}
}
break;
case Editing: //in the editing mode, joystick directional input adds and removes characters from the string, while up and down changes characters
//pressing the joystick button will switch the device into the Print Confirmation mode
// Editing mode
if (prevState != Editing) {
lcd.clear();
prevState = Editing;
}
//lcd.clear();
lcd.setCursor(0, 0);
lcd.print(":");
lcd.setCursor(1, 0);
lcd.print(text);
// Check if the joystick is moved up (previous letter) or down (next letter)
if (joyUp) { //UP (previous character)
Serial.println(currentCharacter);
if (currentCharacter > 0) {
currentCharacter--;
lcd.print(alphabet[currentCharacter]);
//Serial.println("Character UP");
}
delay(250); // Delay to prevent rapid scrolling
} else if (joyDown) { //DOWN (next character)
Serial.println(currentCharacter);
if (currentCharacter < (alphabetSize - 1)) {
currentCharacter++; //increment character value
lcd.print(alphabet[currentCharacter]);
//Serial.println("Character DOWN");
}
delay(250); // Delay to prevent rapid scrolling
} else {
if (millis() % 600 < 450) {
lcd.print(alphabet[currentCharacter]);
} else {
lcd.print(" ");
}
}
// Check if the joystick is moved left (backspace) or right (add space)
if (joyLeft) {
// LEFT (backspace)
if (text.length() > 0) {
text.remove(text.length() - 1);
lcd.setCursor(0, 0);
lcd.print(MENU_CLEAR); //clear and reprint the string so characters dont hang
lcd.setCursor(1, 0);
lcd.print(text);
}
delay(250); // Delay to prevent rapid multiple presses
} else if (joyRight) { //RIGHT adds a space or character to the label
if (currentCharacter == 0) {
text += ' '; //add a space if the character is _
} else {
text += alphabet[currentCharacter]; //add the current character to the text
currentCharacter = 0;
}
delay(250); // Delay to prevent rapid multiple presses
}
if (button1.isPressed()) {
// Single click: Add character and reset alphabet scroll
if (currentCharacter == 0) {
text += ' '; //add a space if the character is _
} else {
text += alphabet[currentCharacter]; //add the current character to the text
currentCharacter = 0; // reset for the next character
}
lcd.clear();
currentState = PrintConfirmation;
prevState = Editing;
}
break;
case PrintConfirmation:
// Print confirmation mode
if (prevState == Editing) {
lcd.setCursor(0, 0); //move cursor to the first line
lcd.print(PRINT_CONF); //print menu text
lcd.setCursor(0, 1); // move cursor to the second line
lcd.print(" YES NO ");
lcd.setCursor(2, 1);
cursorPosition = 2;
prevState = PrintConfirmation;
}
//the following two if statements help move the blinking cursor from one option to the other.
if (joyLeft) { //left
lcd.setCursor(0, 1);
lcd.print(" YES NO ");
lcd.setCursor(2, 1);
cursorPosition = 2;
delay(200);
} else if (joyRight) { //right
lcd.setCursor(0, 1);
lcd.print(" YES NO ");
lcd.setCursor(10, 1);
cursorPosition = 10;
delay(200);
}
lcd.setCursor(cursorPosition, 1);
if (millis() % 600 < 400) { // Blink every 500 ms
lcd.print(">");
} else {
lcd.print(" ");
}
if (button1.isPressed()) { //handles clicking options in print confirmation
if (cursorPosition == 2) { //proceed to printing if clicking yes
lcd.clear();
currentState = Printing;
prevState = PrintConfirmation;
} else if (cursorPosition == 10) { //return to editing if you click no
lcd.clear();
currentState = Editing;
prevState = PrintConfirmation;
}
}
break;
case Printing:
if (text)
if (text == "A") {
text = "B";
}
// Printing mode
if (prevState == PrintConfirmation) {
lcd.setCursor(0, 0);
lcd.print(PRINTING); //update screen
}
// ----------------------------------------------- plot text
plotText(text, xpos, ypos);
line(xpos + space, 0, 0); // move to new line
xpos = 0;
ypos = 0;
text = "";
yStepper.step(-2250);
releaseMotors();
lcd.clear();
currentState = Editing;
prevState = Printing;
break;
}
}
void plotText(String &str, int x, int y) { //takes in our label as a string, and breaks it up by character for plotting
int pos = 0;
Serial.println("plot string");
Serial.println(str);
for (int i = 0; i < str.length(); i++) { //for each letter in the string (expressed as "while i is less than string length")
char c = char(str.charAt(i)); //store the next character to plot on it's own
if (byte(c) != 195) {
if (c == ' ') { //if it's a space, add a space.
pos += space;
} else {
plotCharacter(c, x + pos, y);
pos += space; //scale is multiplied by 4 here to convert it to steps (because it normally get's multiplied by a coordinate with a max of 4)
if (c == 'I' || c == 'i') pos -= (scale * 4) / 1.1;
if (c == ',') pos -= (scale * 4) / 1.2;
}
}
}
Serial.println();
releaseMotors();
}
void plotCharacter(char c, int x, int y) {
Serial.print("letter: ");
Serial.println(c);
const Character* ch = getCharacter(c);
// for (int i = 0; i < 14; i++) { // go through each vector of the character
// int v = vector[character][i];
for (uint8_t i = 0; i < ch->length; i++) {
int v = ch->data[i];
if (v == 200) { // no more vectors in this array
break;
}
if (v == 222) { // plot single point
plot(true);
delay(50);
plot(false);
} else {
int draw = 0;
if (v > 99) {
draw = 1;
v -= 100;
}
int cx = v / 10; // get y ...
int cy = v - cx * 10; // and x
//if(cx > 0) cx = 1;
// 1: Normalize
int x_start = x;
int x_end = x + cx * x_scale;
int y_start = y;
int y_end = y + cy * y_scale * 3.5; //we multiply by 3.5 here to equalize the Y output to match X,
//this is because the Y lead screw covers less distance per-step than the X motor wheel (about 3.5 times less haha)
bool switched = false;
Serial.print("Scale: ");
Serial.print(scale);
Serial.print(" ");
Serial.print("X Goal: ");
Serial.print(x_end);
Serial.print(" ");
Serial.print("Y Goal: ");
Serial.print(y_end);
Serial.print(" ");
Serial.print("Draw: ");
Serial.println(draw);
line(x_end, y_end, draw);
}
}
}
void line(int newx, int newy, bool drawing) {
//this function is an implementation of bresenhams line algorithm
//this algorithm basically finds the slope between any two points, allowing us to figure out how many steps each motor should do to move smoothly to the target
//in order to do this, we give this function our next X (newx) and Y (newy) coordinates, and whether the pen should be up or down (drawing)
if (drawing < 2) { //checks if we should be drawing and puts the pen up or down based on that.
plot(drawing); // dashed: 0= don't draw / 1=draw / 2... = draw dashed with variable dash width
} else {
plot((stepCount / drawing) % 2); //can do dashed lines, but for now this isn't doing anything since we're only sending 0 or 1.
}
int i;
long over = 0;
long dx = newx - xpos; //calculate the difference between where we are (xpos) and where we want to be (newx)
long dy = newy - ypos;
int dirx = dx > 0 ? -1 : 1; //this is called a ternary operator, it's basically saying: if dx is greater than 0, then dirx = -1, if dx is less than or equal to 0, dirx = 1.
int diry = dy > 0 ? 1 : -1; //this is called a ternary operator, it's basically saying: if dy is greater than 0, then diry = 1, if dy is less than or equal to 0, diry = -1.
//the reason one of these ^ is inverted logic (1/-1) is due to the direction these motors rotate in the system.
dx = abs(dx); //normalize the dx/dy values so that they are positive.
dy = abs(dy); //abs() is taking the "absolute value" - basically it removes the negative sign from negative numbers
//the following nested If statements check which change is greater, and use that to determine which coordinate (x or y) get's treated as the rise or the run in the slope calculation
//we have to do this because technically bresenhams only works for the positive quandrant of the cartesian coordinate grid,
// so we are just flipping the values around to get the line moving in the correct direction relative to it's current position (instead of just up an to the right)
if (dx > dy) {
over = dx / 2;
for (i = 0; i < dx; i++) { //for however much our current position differs from the target,
xStepper.step(dirx); //do a step in that direction (remember, dirx is always going to be either 1 or -1 from the ternary operator above)
// Serial.print("Xsteps: ");
// Serial.print(dirx);
// Serial.print(" ");
over += dy;
if (over >= dx) {
over -= dx;
// Serial.print("Ysteps: ");
// Serial.println(diry);
yStepper.step(diry);
}
//delay(1);
}
} else {
over = dy / 2;
for (i = 0; i < dy; i++) {
yStepper.step(diry);
// Serial.print("Ysteps: ");
// Serial.print(diry);
// Serial.print(" ");
over += dx;
if (over >= dy) {
over -= dy;
// Serial.print("Xsteps: ");
// Serial.println(dirx);
xStepper.step(dirx);
}
//delay(1);
}
}
xpos = newx; //store positions
ypos = newy; //store positions
}
void plot(boolean penOnPaper) { //used to handle lifting or lowering the pen on to the tape
if (penOnPaper) { //if the pen is already up, put it down
angle = 80;
} else { //if down, then lift up.
angle = 25;
}
servo.write(angle); //actuate the servo to either position.
if (penOnPaper != pPenOnPaper) delay(50); //gives the servo time to move before jumping into the next action
pPenOnPaper = penOnPaper; //store the previous state.
}
void penUp() { //singular command to lift the pen up
servo.write(25);
}
void penDown() { //singular command to put the pen down
servo.write(80);
}
void releaseMotors() {
for (int i = 0; i < 4; i++) { //deactivates all the motor coils
digitalWrite(xPins[i], 0); //just picks each motor pin and send 0 voltage
digitalWrite(yPins[i], 0);
}
plot(false);
}
void homeYAxis() {
yStepper.step(-3000); //lowers the pen holder to it's lowest position.
}
void resetScreen() {
lcd.clear(); // clear LCD
lcd.setCursor(0, 0); // set cursor to row 0 column 0
lcd.print(": ");
lcd.setCursor(1, 0); //move cursor down to row 1 column 0
cursorPosition = 1;
}
Comments