First Test env

This commit is contained in:
Emanuele Trabattoni
2026-03-27 12:49:20 +01:00
parent a0710f7ee7
commit a210d808da
8 changed files with 292 additions and 86 deletions

View File

@@ -9,8 +9,8 @@
; https://docs.platformio.org/page/projectconf.html
[env:esp32-s3-n16r8]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip
board = esp32-s3-n16r8
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip
framework = arduino
lib_deps =
hideakitai/DebugLog@^0.8.4
@@ -25,14 +25,10 @@ upload_speed = 921600
build_type = release
[env:esp32-s3-n16r8-debug]
platform = ${env:esp32-s3-n16r8.platform}
board = ${env:esp32-s3-n16r8.board}
platform = ${env:esp32-s3-n16r8.platform}
framework = ${env:esp32-s3-n16r8.framework}
lib_deps =
${env:esp32-s3-n16r8.lib_deps}
hideakitai/PCA95x5@^0.1.3
adafruit/Adafruit SSD1306@^2.5.16
garfius/Menu-UI@^1.2.0
lib_deps = ${env:esp32-s3-n16r8.lib_deps}
board_build.partitions = partitions/default_16MB.csv
board_build.psram = enabled
monitor_speed = 115200
@@ -46,3 +42,27 @@ build_flags =
-fno-ipa-sra
-fno-tree-sra
-fno-builtin
[env:esp32-devtest-debug]
board = esp32dev
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip
framework = arduino
lib_deps =
hideakitai/DebugLog@^0.8.4
bblanchon/ArduinoJson@^7.4.2
hideakitai/PCA95x5@^0.1.3
adafruit/Adafruit SSD1306@^2.5.16
garfius/Menu-UI@^1.2.0
board_build.flash_size = 4MB
board_build.partitions = default.csv
monitor_speed = 115200
build_type = debug
build_flags =
-O0
-g3
-ggdb
-fno-inline
-fno-ipa-sra
-fno-tree-sra
-fno-builtin

View File

@@ -27,32 +27,20 @@
#define OUT_A34_N SING_7
struct Devices {
AD5292* pot_a, pot_b;
ADS1256* adc_a, adc_b;
Adafruit_SSD1306* lcd;
PCA9555* io;
AD5292 *pot_a = NULL, *pot_b = NULL;
ADS1256 *adc_a = NULL, *adc_b = NULL;
Adafruit_SSD1306* lcd = NULL;
PCA9555* io = NULL;
};
inline float adcAReadChannel(ADS1256* adc, const uint32_t ch){
inline float adcReadChannel(ADS1256* adc, const uint32_t drdy_pin, const uint32_t ch){
adc->setMUX(ch);
// scarta 3 conversioni
for (int i = 0; i < 3; i++) {
while (digitalRead(ADC_A_DRDY));
while (digitalRead(drdy_pin));
adc->readSingle();
}
// ora lettura valida a 30kSPS → ~100 µs di settling
while (digitalRead(ADC_A_DRDY));
return adc->convertToVoltage(adc->readSingle());
}
inline float adcBReadChannel(ADS1256* adc, const uint32_t ch){
adc->setMUX(ch);
// scarta 3 conversioni
for (int i = 0; i < 3; i++) {
while (digitalRead(ADC_B_DRDY));
adc->readSingle();
}
// ora lettura valida a 30kSPS → ~100 µs di settling
while (digitalRead(ADC_B_DRDY));
while (digitalRead(drdy_pin));
return adc->convertToVoltage(adc->readSingle());
}

View File

@@ -1,8 +1,15 @@
#pragma once
#define TEST
#include <Arduino.h>
#include <map>
#include "soc/gpio_struct.h"
#include "pins.h"
#ifndef TEST
#include "pins.h"
#else
#include "pins_test.h"
#endif
#define CORE_0 0
#define CORE_1 1
@@ -16,15 +23,28 @@
#define TRIG_FLAG_A12N (1 << 2)
#define TRIG_FLAG_A34P (1 << 1)
#define TRIG_FLAG_A34N (1 << 3)
#ifndef TEST
#define TRIG_FLAG_B12P (1 << 4)
#define TRIG_FLAG_B12N (1 << 6)
#define TRIG_FLAG_B34P (1 << 5)
#define TRIG_FLAG_B34N (1 << 7)
#endif
#define SPARK_FLAG_A12 (1 << 8)
#define SPARK_FLAG_A34 (1 << 9)
#ifndef TEST
#define SPARK_FLAG_B12 (1 << 10)
#define SPARK_FLAG_B34 (1 << 11)
#endif
static const std::map<const uint32_t, const char*> names = {
{TRIG_FLAG_A12P, "TRIG_A12P"},
{TRIG_FLAG_A12N, "TRIG_A12N"},
{TRIG_FLAG_A34P, "TRIG_A34P"},
{TRIG_FLAG_A34N, "TRIG_A34N"},
{SPARK_FLAG_A12, "SPARK_A12"},
{SPARK_FLAG_A34, "SPARK_A34"},
};
// Task handle
TaskHandle_t trigA_TaskHandle = NULL;
@@ -52,7 +72,7 @@ enum softStartStatus
NORMAL
};
typedef struct coilsStatus
struct coilsStatus
{
int64_t trig_time;
int64_t spark_time;
@@ -64,7 +84,7 @@ typedef struct coilsStatus
};
// Task internal Status
typedef struct ignitionBoxStatus
struct ignitionBoxStatus
{
// coils pairs for each ignition
coilsStatus coils12;
@@ -84,10 +104,12 @@ void initTriggerPinMapping()
pin2trig[TRIG_A12N] = TRIG_FLAG_A12N;
pin2trig[TRIG_A34P] = TRIG_FLAG_A34P;
pin2trig[TRIG_A34N] = TRIG_FLAG_A34N;
#ifndef TEST
pin2trig[TRIG_B12P] = TRIG_FLAG_B12P;
pin2trig[TRIG_B12N] = TRIG_FLAG_B12N;
pin2trig[TRIG_B34P] = TRIG_FLAG_B34P;
pin2trig[TRIG_B34N] = TRIG_FLAG_B34N;
#endif
};
static uint32_t pin2spark[49];
@@ -95,8 +117,10 @@ void initSparkPinMapping()
{
pin2spark[SPARK_A12] = SPARK_FLAG_A12;
pin2spark[SPARK_A34] = SPARK_FLAG_A34;
#ifndef TEST
pin2spark[SPARK_B12] = SPARK_FLAG_B12;
pin2spark[SPARK_B34] = SPARK_FLAG_B34;
#endif
};
// =====================
@@ -110,7 +134,11 @@ void IRAM_ATTR trig_isr_a()
if (!trigA_TaskHandle)
return; // exit if task is not running
#ifndef TEST
uint32_t status = GPIO.status;
#else
uint32_t status = GPIO.status1.val;
#endif
uint32_t pickup_flags = 0;
while (status)
@@ -125,7 +153,7 @@ void IRAM_ATTR trig_isr_a()
if (pickup_flags & TRIG_FLAG_A34P)
ignA_status.coils34.trig_time = time_us;
xTaskNotifyFromISR(trigA_TaskHandle, pickup_flags, eSetBits, &xHigherPriorityTaskWoken);
xTaskNotifyFromISR(trigA_TaskHandle, GPIO.status1.val, eSetBits, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
@@ -135,15 +163,21 @@ void IRAM_ATTR spark_a()
volatile const int64_t time_us = esp_timer_get_time();
if (!trigA_TaskHandle)
return;
#ifndef TEST
uint32_t spark_flag = GPIO.status1.val & SPARK_A12 ? SPARK_FLAG_A12 : SPARK_FLAG_A34;
#else
uint32_t spark_flag = GPIO.status & SPARK_A12 ? SPARK_FLAG_A12 : SPARK_FLAG_A34;
#endif
if (spark_flag & SPARK_FLAG_A12)
ignA_status.coils12.spark_time = time_us;
if (spark_flag & SPARK_FLAG_A34)
ignA_status.coils12.spark_time = time_us;
xTaskNotifyFromISR(trigA_TaskHandle, spark_flag, eSetBits, &xHigherPriorityTaskWoken);
xTaskNotifyFromISR(trigA_TaskHandle, GPIO.status, eSetBits, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
#ifndef TEST
void IRAM_ATTR trig_isr_b()
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
@@ -151,7 +185,11 @@ void IRAM_ATTR trig_isr_b()
if (!trigB_TaskHandle)
return; // exit if task is not running
#ifndef TEST
uint32_t status = GPIO.status;
#else
uint32_t status = GPIO.status1.val;
#endif
uint32_t pickup_flags = 0;
while (status)
@@ -184,3 +222,4 @@ void IRAM_ATTR spark_b()
xTaskNotifyFromISR(trigB_TaskHandle, spark_flag, eSetBits, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
#endif

View File

@@ -7,9 +7,8 @@
#include <SPI.h>
// Definitions
#include <pins.h>
#include <channels.h>
#include <tasks.h>
#include <channels.h>
#include <devices.h>
void setup()
@@ -23,7 +22,11 @@ void setup()
// Print Processor Info
LOG_INFO("ESP32 Chip:", ESP.getChipModel());
LOG_INFO("ESP32 PSram:", ESP.getPsramSize());
if (psramFound()){
LOG_INFO("ESP32 PSram Found");
LOG_INFO("ESP32 PSram:", ESP.getPsramSize());
psramInit();
}
LOG_INFO("ESP32 Flash:", ESP.getFlashChipSize());
LOG_INFO("ESP32 Heap:", ESP.getHeapSize());
LOG_INFO("ESP32 Sketch:", ESP.getFreeSketchSpace());
@@ -34,29 +37,49 @@ void setup()
// Initialize Interrupt pins on SPARK detectors
initSparkPinInputs();
initSparkPinMapping();
// Init SPI interface
SPI.begin();
}
void loop()
{
// global variables
bool running = true;
rtTaskParams taskA_params, taskB_params;
Devices dev;
// Init devices
dev.adca = new ADS1256(ADC_DRDY, ADC_RST, ADC_SYNC, ADC_CS, 2.5, &SPI);
dev.adc->InitializeADC();
dev.adc->setPGA(PGA_1);
dev.adc->setDRATE(DRATE_1000SPS);
#ifndef TEST
// Init 2 SPI interfaces
SPIClass SPI_A(FSPI);
SPIClass SPI_B(HSPI);
if (!SPI_A.begin(SPI_A_SCK, SPI_A_MISO, SPI_A_MOSI) || !SPI_B.begin(SPI_A_SCK, SPI_A_MISO, SPI_A_MOSI)) {
LOG_ERROR("Unable to Initialize SPI Busses");
LOG_ERROR("5 seconds to restart...");
vTaskDelay(pdMS_TO_TICKS(5000));
esp_restart();
}
#endif
pinMode(POT_A_CS, OUTPUT); // Temporary!
pinMode(POT_B_CS, OUTPUT); // Temporary!
LOG_INFO("Init SPI [OK]");
// Init ADC_A
// dev.adc_a = new ADS1256(ADC_A_DRDY, ADC_A_RST, ADC_A_SYNC, ADC_A_CS, 2.5, &SPI_A);
// dev.adc_a->InitializeADC();
// dev.adc_a->setPGA(PGA_1);
// dev.adc_a->setDRATE(DRATE_1000SPS);
// Init ADC_B
// dev.adc_a = new ADS1256(ADC_B_DRDY, ADC_B_RST, ADC_B_SYNC, ADC_B_CS, 2.5, &SPI_B);
// dev.adc_a->InitializeADC();
// dev.adc_a->setPGA(PGA_1);
// dev.adc_a->setDRATE(DRATE_1000SPS);
// Ignition A on Core 0
auto ignA_task_success = xTaskCreatePinnedToCore(
ignitionA_task,
"ignitionA_task",
TASK_STACK,
(void *)&dev,
(void *)&taskA_params,
TASK_PRIORITY,
&trigA_TaskHandle,
CORE_0);
@@ -67,7 +90,7 @@ void loop()
// ignitionB_task,
// "ignitionB_task",
// TASK_STACK,
// (void *)&dev,
// (void *)&taskB_params,
// TASK_PRIORITY, // priorità leggermente più alta
// &trigA_TaskHandle,
// CORE_1);
@@ -82,8 +105,14 @@ void loop()
LOG_INFO("Real Time Tasks A&B initialized");
////////////////////// MAIN LOOP //////////////////////
uint32_t count(0);
while (running)
{
//digitalWrite(POT_B_CS, HIGH);
//delay(500);
//LOG_INFO("Main Loop [", count++, "]");
//digitalWrite(POT_B_CS, LOW);
//delay(500);
}
if (trigA_TaskHandle)

View File

@@ -1,4 +1,5 @@
#pragma once
#include <Arduino.h>
// =====================
// USB (RISERVATA)
@@ -18,7 +19,7 @@
#define LED 48
// =====================
// STRAPPING (NON USARE)
// STRAPPING CRITICI (NON USARE)
// =====================
// 0, 3
@@ -43,7 +44,7 @@
#define SCL 9
// =====================
// ADC CONTROL (NO CONFLICT)
// ADC CONTROL
// =====================
#define ADC_A_CS 4
#define ADC_A_DRDY 5
@@ -64,28 +65,28 @@
// =====================
// TRIGGER INPUT INTERRUPTS
// =====================
#define TRIG_A12P 21
#define TRIG_A12N 33
#define TRIG_A34P 34
#define TRIG_A34N 38
#define TRIG_B12P 39
#define TRIG_B12N 40
#define TRIG_B34P 41
#define TRIG_B34N 42
#define TRIG_A12P 18
#define TRIG_A12N 21
#define TRIG_A34P 33
#define TRIG_A34N 34
#define TRIG_B12P 38
#define TRIG_B12N 39
#define TRIG_B34P 40
#define TRIG_B34N 41
// =====================
// SPARK DETECT INPUTS (INPUT ONLY SAFE)
// SPARK DETECT INPUTS
// =====================
#define SPARK_A12 45
#define SPARK_A34 46
#define SPARK_B12 47
#define SPARK_B34 18
#define SPARK_A12 42
#define SPARK_A34 45 // OK (strapping ma consentito)
#define SPARK_B12 46 // OK (strapping ma consentito)
#define SPARK_B34 47
// =====================
// PCA9555 (I2C EXPANDER)
// =====================
// --- RESET LINES (ora su expander) ---
// --- RESET LINES ---
#define RST_A12P 0
#define RST_A12N 1
#define RST_A34P 2
@@ -95,11 +96,11 @@
#define RST_B34P 6
#define RST_B34N 7
// --- RELAY LINES ---
// --- RELAY ---
#define A_RELAY 8
#define B_RELAY 9
// --- STATUS OUTPUT ---
// --- STATUS / BUTTON ---
#define BTN_3 10
#define BTN_4 11
#define STA_1 12
@@ -126,4 +127,4 @@ inline void initSparkPinInputs()
pinMode(SPARK_A34, INPUT);
pinMode(SPARK_B12, INPUT);
pinMode(SPARK_B34, INPUT);
}
}

View File

@@ -0,0 +1,71 @@
#pragma once
#include <Arduino.h>
// =====================
// UART DEBUG
// =====================
#define UART_TX 1 // TX0 (USB seriale)
#define UART_RX 3 // RX0
// =====================
// SPI BUS
// =====================
#define SPI_A_MOSI 23
#define SPI_A_MISO 19
#define SPI_A_SCK 18
// =====================
// I2C BUS
// =====================
#define SDA 21
#define SCL 22
// =====================
// ADC CONTROL (SPI + interrupt safe)
// =====================
#define ADC_A_CS 5 // chip select
#define ADC_A_DRDY 34 // input only + interrupt perfetto
#define ADC_A_RST 27 // output
#define ADC_A_SYNC 26 // output
// =====================
// DIGITAL OUT
// =====================
#define POT_A_CS 25
#define POT_B_CS 33
// =====================
// TRIGGER INPUT INTERRUPTS
// =====================
#define TRIG_A12P 35
#define TRIG_A12N 32
#define TRIG_A34P 39
#define TRIG_A34N 36
// =====================
// SPARK DETECT INTERRUPTS
// =====================
#define SPARK_A12 4
#define SPARK_A34 2
// Init Pin Functions
inline void initTriggerPinsInputs()
{
pinMode(TRIG_A12P, INPUT_PULLDOWN);
pinMode(TRIG_A12N, INPUT_PULLDOWN);
pinMode(TRIG_A34P, INPUT_PULLDOWN);
pinMode(TRIG_A34N, INPUT_PULLDOWN);
}
inline void initSparkPinInputs()
{
pinMode(SPARK_A12, INPUT_PULLDOWN);
pinMode(SPARK_A34, INPUT_PULLDOWN);
}

View File

@@ -3,6 +3,7 @@
// Arduino Libraries
#include <Arduino.h>
#include <DebugLog.h>
#include "utils.h"
// ISR
#include "isr.h"
@@ -14,15 +15,27 @@
static bool rt_task_running = true;
const auto spark_timeout_max = 1; // convert to microsecond timer
// RT task parameters
struct rtTaskParams {
bool rt_running;
Devices *dev;
};
void ignitionA_task(void *pvParameters) {
if (!pvParameters) {
LOG_ERROR("Null device parameters");
LOG_ERROR("Null task parameters");
vTaskDelete(NULL);
}
Devices* dev = (Devices*) pvParameters;
ADS1256* adc = dev->adc;
// Task Parameters and Devices
rtTaskParams* params = (rtTaskParams*) pvParameters;
Devices* dev = (Devices*) params->dev;
ADS1256* adc = dev->adc_a;
// Global task variables
uint32_t pickup_flag = 0;
uint32_t spark_flag = 0;
// Ignition A Interrupts
attachInterrupt(TRIG_A12P, trig_isr_a, RISING);
@@ -32,24 +45,37 @@ void ignitionA_task(void *pvParameters) {
attachInterrupt(SPARK_A12, spark_a, RISING);
attachInterrupt(SPARK_A34, spark_a, RISING);
uint32_t pickup_flag;
uint32_t spark_flag;
while (rt_task_running) {
// WAIT FOR PICKUP SIGNAL
xTaskNotifyWait(
0x00, // non pulire all'ingresso
0x000000FF, // pulisci i primi 8 bit
ULONG_MAX, // non pulire all'ingresso
ULONG_MAX, // pulisci i primi 8 bit
&pickup_flag, // valore ricevuto
portMAX_DELAY
);
LOG_ERROR("Pickup Interrupt Status", printBits(pickup_flag).c_str());
if (!names.contains(pickup_flag)) {
continue;
} else {
LOG_INFO("Pickup Trigger: ", names.at(pickup_flag));
}
// WAIT FOR SPARK TO HAPPEN
auto spark_timeout = xTaskNotifyWait(
0x00, // non pulire all'ingresso
0x0000FF00, // pulisci gli 8 bit successivi
ULONG_MAX, // non pulire all'ingresso
ULONG_MAX, // pulisci gli 8 bit successivi
&spark_flag, // valore ricevuto
spark_timeout_max
);
if (spark_timeout == pdPASS){ //otherwise timeout
LOG_ERROR("Spark Interrupt Status", printBits(spark_flag).c_str());
if (!names.contains(spark_flag)) {
continue;
} else {
LOG_INFO("Spark Trigger:", names.at(spark_flag));
}
} else {
LOG_INFO("Spark Timeout");
}
// A trigger from pickup 12 is followed by a spark event on 34 or vice versa pickup 34 triggers spark on 12
if ((pickup_flag == TRIG_FLAG_A12P || pickup_flag == TRIG_FLAG_A12N) && spark_flag != SPARK_A12) {
@@ -79,11 +105,13 @@ void ignitionA_task(void *pvParameters) {
case TRIG_FLAG_A12P:
case TRIG_FLAG_A34P:
{
LOG_INFO("Trigger Pickup POSITIVE");
// Timeout not occourred, expected POSITIVE edge spark OCCOURRED
if (spark_timeout == pdPASS) {
c->spark_delay = c->spark_time - c->trig_time;
c->soft_start_status = softStartStatus::NORMAL; // because spark on positive edge
c->spark_status = sparkStatus::SPARK_POS_OK; // do not wait for spark on negative edge
LOG_INFO("Trigger Spark POSITIVE");
}
// Timeout occourred, expected POSITIVE edge spark NOT OCCOURRED
else if (spark_timeout == pdFAIL) {
@@ -98,11 +126,13 @@ void ignitionA_task(void *pvParameters) {
case TRIG_FLAG_A34N:
{
const bool expected_negative12 = c->spark_status == sparkStatus::SPARK_NEG_WAIT;
LOG_INFO("Trigger Pickup NEGATIVE");
// Timeout not occourred, expected NEGATIVE edge spark OCCOURRED
if (spark_timeout == pdPASS && expected_negative12) {
c->spark_delay = c->spark_time - c->trig_time;
c->soft_start_status = softStartStatus::SOFT_START;
c->spark_status == sparkStatus::SPARK_NEG_OK;
LOG_INFO("Trigger Spark NEGATIVE");
}
// Timeout occourred, expected POSITIVE edge spark NOT OCCOURRED
else if (spark_timeout == pdFAIL && expected_negative12) {
@@ -125,18 +155,27 @@ void ignitionA_task(void *pvParameters) {
if (new_data) {
vTaskDelay(pdMS_TO_TICKS(1)); // delay 1ms to allow peak detectors to charge for negative cycle
// read adc channels: pickup12, out12 [ pos + neg ]
ignA_status.coils12.pickup_volts = adcReadChannel(adc, IN_A12_P);
ignA_status.coils12.output_volts = adcReadChannel(adc, PICKUP_OUT_A12);
ignA_status.coils34.pickup_volts = adcReadChannel(adc, PICKUP_IN_A34);
ignA_status.coils34.output_volts = adcReadChannel(adc, PICKUP_OUT_A34);
// reset peak detectors -> gli output sono sull'expander
//digitalWrite(RST_A12P, HIGH);
//digitalWrite(RST_A12N, HIGH);
//vTaskDelay(pdMS_TO_TICKS(1));
//digitalWrite(RST_A12P, HIGH);
//digitalWrite(RST_A12N, HIGH);
if (adc) // read only if adc initialized
{
ignA_status.coils12.pickup_volts = adcReadChannel(adc, ADC_A_DRDY, IN_A12_P);
ignA_status.coils12.output_volts = adcReadChannel(adc, ADC_A_DRDY, IN_A12_N);
ignA_status.coils34.pickup_volts = adcReadChannel(adc, ADC_A_DRDY, IN_A34_P);
ignA_status.coils34.output_volts = adcReadChannel(adc, ADC_A_DRDY, IN_A34_N);
} else {
LOG_WARN("ADC not initialized, skipping conversion");
}
// reset peak detectors -> gli output sono sull'expander, temporaneo uso il PC del pot non collegato
digitalWrite(POT_A_CS, HIGH);
// digitalWrite(RST_A12P, HIGH);
// digitalWrite(RST_A12N, HIGH);
// digitalWrite(RST_A34P, HIGH);
// digitalWrite(RST_A34N, HIGH);
vTaskDelay(1);
digitalWrite(POT_A_CS, HIGH);
// digitalWrite(RST_A12P, LOW);
// digitalWrite(RST_A12N, LOW);
// digitalWrite(RST_A34P, LOW);
// digitalWrite(RST_A34N, LOW);
// save on circluar buffer 12
}
@@ -153,6 +192,7 @@ void ignitionA_task(void *pvParameters) {
vTaskDelete(NULL);
}
#ifndef TEST
void ignitionB_task(void *pvParameters) {
uint32_t notifiedValue;
// Ignition B Interrupts
@@ -184,4 +224,5 @@ void ignitionB_task(void *pvParameters) {
LOG_ERROR("Invalid B Interrupt: ", notifiedValue);
}
}
}
}
#endif

17
RotaxMonitor/src/utils.h Normal file
View File

@@ -0,0 +1,17 @@
#pragma once
#include <Arduino.h>
#include <string>
std::string printBits(uint32_t value) {
std::string result;
for (int i = 31; i >= 0; i--) {
// ottieni il singolo bit
result += ((value >> i) & 1) ? '1' : '0';
// aggiungi uno spazio ogni 8 bit, tranne dopo l'ultimo
if (i % 8 == 0 && i != 0) {
result += ' ';
}
}
return result;
}