Narrative
Arduino is truly remarkable; I have a deep fondness for it. It can be likened to the “Lego” of the electronics and automation world. Presently, I’m engrossed in a project where numerous MCU boards are orchestrated to form an interconnected network of processors, aptly named NetP. These boards establish communication via the i2c bus. The selection comprised Arduino Mini Pro boards operating at a voltage of 3.3V. To facilitate the transfer of sketches from the Arduino IDE to the Mini Pros, a USB to serial FTDI board is indispensable. Additionally, in order to accommodate the keyboard, a TTL level converter is employed to shift from 3.3V to 5V.
At the moment, several functionalities have been successfully implemented, while there remains a list of pending tasks. Furthermore, certain aspects are awaiting ingenious solutions, aligned with our aspirations.
Accomplishments thus far include:
The most prominent component is the console, comprised of a keyboard and a scrolling display. This facilitates interaction with the nearby MCU, Netp0 (Console), which, in turn, enables communication with other constituents of the local i2c network—Netp1 (SD), Netp2 (Updater), Netp3 (Clock), and so forth.
Alternatively, you have the option to forgo the console’s display and keyboard by establishing a Bluetooth connection with a smartphone. This enables remote control, and I have impeccably configured the app “Serial Bluetooth Terminal 1.31” for this purpose, although alternative apps are also viable options.
Each member of the Netp MCU ensemble is assigned distinct attributes: an individualized i2c bus address (0x0a), a designated name (CONSOLE), a unique ID (p0), and a corresponding number (0). It’s worth noting that the i2c bus may also accommodate additional elements, currently limited to an EEPROM and a Real Time Clock board. It’s imperative to allocate separate i2c addresses for each member or constituent.
The overarching design principle is plug-and-play functionality. Adding a new Netp member to the setup seamlessly integrates it into the network, a process readily discernible through the “scan” command, which identifies newly connected members.
Functionality of the CONSOLE (p0):
- scan: Initiating a scan [inquiry into the i2c bus, e.g., scan, generates a roster of active addresses on the bus presented as “DDD (0xEE)”… culminates with “‘n’ found”].
- ckpower: Querying power status [e.g., ckpower, yields “v3=3.3 v5=5.0 vraw=7.0-12.0”].
- ?: Requesting command elucidation [e.g., ?, retrieves a catalogue of available commands on the board along with available memory].
- reset: Enforcing a reboot [e.g., reset compels the MPU to undergo a reset].
- <pxname> <cmd>: Dispatching <cmd> to processor <pxname> [e.g., SD dir /, delivers the outcome of <cmd> from processor <pxname>].
The “Scan” command is automatically activated at 30-second intervals, facilitating seamless plug-and-play integration. To ascertain availability, simply execute the “scan” command, revealing names and addresses of active components. Employ the “?” command to unveil the array of implemented commands on the console. For insights into the commands operational on the SD member, initiate the “p1 ?” command. If you wish to obtain a listing of files on the SD, executing “p1 dir /” will suffice—similarly for other functionalities.
Functionality of the SD (p1):
- ?: Request assistance [e.g., ?, provides an inventory of accessible commands on the board along with available memory].
- mw/mwa: mw <FileName>=<Value> [initiates composition/modification of Value within file name, e.g., mw temp01=24 (FileName=Value), yields “written”] (mwa will append Value to the file)
- mr/mra: mr <FileName> [extracts Value from corresponding file name, e.g., mr temp01 (FileName is temp01, Value is 24, returns ‘Value’ or “not found”)] (mra will retrieve multi-line Value from the file)
- reset: reset [initiates MPU reset, e.g., reset]
- dir: dir <path> [scans for file names commencing from ‘path’, e.g., dir /], provides a roster of file ‘names’ along with their ‘size’… concludes with ‘n found’]
- mkdir: mkdir <dirname> [constructs a fresh directory (including relevant subdirectories), e.g., mkdir /music, sdmkdir /logic/params/binary, yields “built” or “error”]
- rmdir: rmdir <dirname> [erases a directory, for example, rmdir /music, produces “removed” or “error or not empty”]
- rename: rename <OldFileName> <NewFileName> [renames a file, i.e. rename myfile.txt bestfile.txt, returns “renamed” or “not found”]
- del: del <FileName> [delete a file, i.e. del myfile.txt, returns “deleted” or “not found”]
- type: type <FileName> <Mode> [reads a file and displays in mode 0=CHR, 1=BIN, 2=HEX, i.e. type myfile.txt CHR, returns the content of the file… and filesize ‘n’ and bytes red ‘n’ at the end]
- eeupl: eeupl <Filename> [read a file and upload it to the EEPROM, write and verify all data, i.e. eeupl /mysketch.hex, returns upload, written, verified…]
As a result of limited memory availability, certain commands have been deactivated, retaining only those essential for my needs. My past encounters have emphasized the importance of refraining from pushing memory usage close to its maximum capacity, as this may lead to unforeseeable MCU behavior. For the SD sketch, I believe I’ve meticulously optimized the code to conserve memory space, but there might still be room for further improvements.
The mw and mr commands function within the /MEMORY/ subdirectory, whereas mwa and mra are applicable within the /LOG/ subdirectory.
Functionality of the UPDATER (p2):
- ?: ? [ask for help, i.e. ?, returns a list of available commands on board and free memory]
- reset: reset [resets MPU, i.e. reset]
Functionality of the CLOCK (p3):
- ?: ? [ask for help, i.e. ?, returns a list of available commands on board and free memory]
- reset: reset [resets MPU, i.e. reset]
- red: red <Mode> [switch on or off the red LED, i.e. red on, returns “ON” or “OFF”]
- green: green <Mode> [switch on or off the green LED, i.e. green on, returns “ON” or “OFF”]
- dht: dht [get temperature and humidity from DHT board, i.e. DHT, returns the values]
- gettemp: gettemp [get temperature from RTC board, i.e. gettemp, returns the temperature value]
- gettime: gettime [get time from RTC board, i.e. gettime, returns the time value as per DD/MM/YYYY HH:MM:SS]
- settime: settime [set the time to RTC board, i.e. settime 31/12/2018 10:20:30, returns the time value as per DD/MM/YYYY HH:MM:SS]
List of Components:
- 4 x Arduino Mini Pro 3.3v
- Micro SD Card adapter
- MicroSD memory card
- RTC zs-042 module
- DHT-11 sensor
- Protoboard power supply from 7-12V to 3.3V and 5V
- 4 x protoboards
- Red LED
- Green LED
- 2 x 470uF 25V capacitors
- 100nF capacitor
- HC05 bluetooth module
- FTDI USB-Serial converter
- Display 2.4″ TFT LCD 240×320 RGB, driver IC ILI9341
- USB mini keyboard
- TTL level converter 3.3V <-> 5V for keyboard USB
- USB female + cable
- Active buzzer
- EEPROM i2c 1024
- Push button
- 100 ohm resistor
- 4 x 5k1 ohm resistors
- 2k2 ohm resistor
- 6k8 resistor
- 2 x 120 ohm resistor
- Many wires..
i2c Communication Bus Protocol:
Netpx members establish communication through the i2c BUS utilizing the Wire.h library, employing a customized protocol based on messages and events.
When a command like “SD dir /” is inputted from the console keyboard, the name “SD” is initially translated into the corresponding address (e.g., 0x0b) by referencing the netRegisterNames[] matrix. This address is then dispatched onto the BUS using the askCommandOnPx() function. On the recipient end, a receiveEvent() event is triggered, leading to the local execution of the requested command and the subsequent transmission of its data. The identifier for these messages is the chr(7) BEL byte.
Furthermore, during the scan command, an additional message is conveyed through the BUS using the whois() function in conjunction with Wire.requestFrom(). This message is employed to solicit the name and supplementary details from a potential Netpx member at a designated address, such as 0x0c. In the absence of a suitable response, the device is categorized as an “other device.” On the receiving side, a netWhoIsEvent() event is triggered. The identifier for these messages is the chr(5) ENQ byte.
Pending Tasks (My Wishlist):
First and foremost, a command request queue is of utmost importance. Currently, when commands are dispatched from the console, the execution of subsequent commands is often neglected if sent in quick succession following the initial one. This proposed feature entails amassing all commands within a queue and subsequently executing them one by one, maintaining their appropriate sequence.
The second enhancement on my agenda involves the implementation of a scheduler. This scheduler would comprise a compilation of commands earmarked for execution at specific times or intervals. This list of commands could be interactively modified through console interactions, endowing a dynamic and responsive scheduling mechanism.
Lastly, my most ambitious aspiration pertains to the facilitation of storing updated or new sketches within a designated directory on the SD card. This revamped setup would involve the UPDATER overseeing the transfer of these sketches to the EEPROM before deployment onto the target MCU. To enable such functionality, the destination MCU, slated for the update, should be equipped to autonomously read from the EEPROM and perform the required autoupdate. However, achieving this functionality necessitates the modification of the conventional bootloader for seamless integration.
Do you have any thoughts or recommendations?
As I gather new developments, enhancements, additional components, or fresh concepts, I will share these updates within these sections. I eagerly await your insights and feedback on this ongoing project.
Code
/* Arduino Net-P (i2c processors network) by Zonca Marco 2020 * 'netMyNAME' ("CLOCK ") max 8 char, 'netMyID' (p9) max 2 char, * address='netMyAddress' (0x16) max 0xFF, 'netMyPx' (9) * * implemented commands (* = to do): HELP | ?, dht, gettemp, gettime, settime, red, green * */ #include <Wire.h> #include <ds3231.h> // RTC real time clock #include <dht.h> const int netMyPx = 9; // netMyPx 0-9 const int netMyAddress = 0x16; // i2c address 0x16=HEX 22=DEC const char *netMyID = "p9"; // netMyID const char *netMyNAME = "CLOCK "; // netMyNAME const int GreenLedPin=8; const int RedLedPin=9; const int DHTPin=14; const int ResetPin=17; // A3 put low for reset char _CR[2]=""; // CR char _BEL[2]=""; // BEL char _ENQ[2]=""; // ENQ char _ACK[2]=""; // ACK char _SPACE[2]=""; // SPACE char _MYADDR[2]=""; // netMyAddress char lineString[65]=""; char recCommand[65]=""; byte recAddress=0; byte recType=0; char data[2]=""; bool isNetDataWaiting=false; bool isKnownCommand = false; bool isRedOn=false; bool isGreenOn=false; bool isBusy=false; struct ts t; // RTC dht DHT; // HG + Temp sensor void setup() { //Serial.begin(38400); Wire.begin(netMyAddress); Wire.onReceive(receiveEvent); // i2c event Wire.onRequest(netWhoIsEvent); // i2c whois _CR[0] = 13; // CR _BEL[0] = 7; // BEL _ENQ[0] = 5; // ENQ _ACK[0] = 6; // ACK _SPACE[0] = 32; // SPACE _MYADDR[0] = netMyAddress; pinMode(GreenLedPin, OUTPUT); pinMode(RedLedPin, OUTPUT); digitalWrite(RedLedPin,LOW); digitalWrite(GreenLedPin,LOW); DS3231_init(DS3231_INTCN); // RTC DHT.read11(DHTPin); // DHT } // end setup() void loop() { if (isNetDataWaiting == true) { execNetCommand(); } } // end loop() void execNetCommand() { // executes received command isBusy=true; char park[65]=""; char command[65]=""; if (recType == _BEL[0]) { // char(7) = BEL command type isKnownCommand=false; if (s_compare(recCommand,"")==0) { // empty command just do nothing isKnownCommand=true; prompt(); } s_substring(park,sizeof(park),recCommand,sizeof(recCommand),0,3); // red if (s_compare(park,"red ") == 0) { s_substring(command,sizeof(command),recCommand,sizeof(recCommand),4,strlen(recCommand)-1); if (strcmp(command, "on") == 0 || strcmp(command, "off") == 0 || strcmp(command, "?") == 0) { isKnownCommand=true; ledred(command); } } s_substring(park,sizeof(park),recCommand,sizeof(recCommand),0,5); // green if (s_compare(park,"green ") == 0) { s_substring(command,sizeof(command),recCommand,sizeof(recCommand),6,strlen(recCommand)-1); if (strcmp(command, "on") == 0 || strcmp(command, "off") == 0 || strcmp(command, "?") == 0) { isKnownCommand=true; ledgreen(command); } } s_substring(park,sizeof(park),recCommand,sizeof(recCommand),0,4); // reset if (s_compare(park,"reset") == 0) { isKnownCommand=true; reset(); } s_substring(park,sizeof(park),recCommand,sizeof(recCommand),0,2); // dht if (s_compare(park,"dht") == 0) { isKnownCommand=true; dht(); } s_substring(park,sizeof(park),recCommand,sizeof(recCommand),0,6); // gettemp if (s_compare(park,"gettemp") == 0) { isKnownCommand=true; gettemp(); } s_substring(park,sizeof(park),recCommand,sizeof(recCommand),0,6); // gettime if (s_compare(park,"gettime") == 0) { isKnownCommand=true; gettime(); } s_substring(park,sizeof(park),recCommand,sizeof(recCommand),0,7); // settime if (s_compare(park,"settime ") == 0) { s_substring(command,sizeof(command),recCommand,sizeof(recCommand),8,strlen(recCommand)-1); if (strlen(command) == 19) { isKnownCommand=true; settime(command); } } s_substring(park,sizeof(park),recCommand,sizeof(recCommand),0,0); // ? if (s_compare(park,"?") == 0) { isKnownCommand=true; help(); } if (isKnownCommand == false) { s_assign(lineString,"unknown cmd",sizeof(lineString)); dispLine(); prompt(); } } // endif recType s_clear(recCommand,sizeof(recCommand)); recType=0; recAddress=0; isNetDataWaiting=false; isBusy=false; } // end execNetCommand() void reset() { // ------------------------------------------------------------ RESET analogWrite(ResetPin,LOW); } void help() { // ---------------------------------------------- HELP s_assign(lineString, "red <on off ?>", sizeof(lineString)); dispLine(); s_assign(lineString, "green <on off ?>", sizeof(lineString)); dispLine(); s_assign(lineString, "dht", sizeof(lineString)); dispLine(); s_assign(lineString, "gettemp", sizeof(lineString)); dispLine(); s_assign(lineString, "gettime", sizeof(lineString)); dispLine(); s_assign(lineString, "settime <DD/MM/YYYY HH:MM:SS>", sizeof(lineString)); dispLine(); s_assign(lineString, "reset", sizeof(lineString)); dispLine(); sprintf(lineString,"(free=%d)",s_freemem()); dispLine(); prompt(); } // end help() void ledred(const char *_s) { // ---------------------------------------------- RED if (strcmp(_s, "on")==0) { digitalWrite(RedLedPin,HIGH); isRedOn=true; } if (strcmp(_s, "off")==0) { digitalWrite(RedLedPin,LOW); isRedOn=false; } if (isRedOn) { sprintf(lineString, "%s","ON"); } else { sprintf(lineString, "%s","OFF"); } dispLine(); prompt(); } // end ledred() void ledgreen(const char *_s) { // ---------------------------------------------- GREEN if (strcmp(_s, "on")==0) { digitalWrite(GreenLedPin,HIGH); isGreenOn=true; } if (strcmp(_s, "off")==0) { digitalWrite(GreenLedPin,LOW); isGreenOn=false; } if (isGreenOn) { sprintf(lineString, "%s","ON"); } else { sprintf(lineString, "%s","OFF"); } dispLine(); prompt(); } // end ledgreen() void dht() { // ---------------------------------------------- DHT float temp=0; int iintt=0; int idect=0; int iinth=0; int idech=0; DHT.read11(DHTPin); // DHT temp = DHT.temperature; // read DHT temp C iintt=temp; idect=float((temp-iintt)*100); temp = DHT.humidity; // read DHT humidity iinth=temp; idech=float((temp-iinth)*100); sprintf (lineString, "%d.%dC %d.%d", iintt, idect, iinth, idech); s_concat(lineString,"%",sizeof(lineString)); dispLine(); prompt(); } // end gettemp() void gettemp() { // ---------------------------------------------- GETTEMP float temp=0; int iint=0; int idec=0; temp = DS3231_get_treg(); // read RTC temp C iint=temp; idec=float((temp-iint)*100); sprintf (lineString, "%d.%dC", iint, idec); dispLine(); prompt(); } // end gettemp() void gettime() { // ---------------------------------------------- GETTIME DS3231_get(&t); // read RTC date & time sprintf(lineString, "%02d/%02d/%04d %02d:%02d:%02d", t.mday, t.mon, t.year, t.hour, t.min, t.sec); dispLine(); prompt(); } // end gettime() void settime(const char* _s) { // ---------------------------------------------- SETTIME char park[5]=""; s_substring(park, sizeof(park), _s, strlen(_s), 0,1); t.mday=atoi(park); s_substring(park, sizeof(park), _s, strlen(_s), 3,4); t.mon=atoi(park); s_substring(park, sizeof(park), _s, strlen(_s), 6,9); t.year=atoi(park); s_substring(park, sizeof(park), _s, strlen(_s), 11,12); t.hour=atoi(park); s_substring(park, sizeof(park), _s, strlen(_s), 14,15); t.min=atoi(park); s_substring(park, sizeof(park), _s, strlen(_s), 17,18); t.sec=atoi(park); DS3231_set(t); // write RTC gettime(); // answer with new time } // end settime() void prompt() { // prompt, ready for commands sprintf(lineString,"%d>\r",netMyPx); dispLine(); } // end prompt() void dispLine() { // print information s_concat(lineString, _CR, sizeof(lineString)); txBackToSender(); // tx s_clear(lineString, sizeof(lineString)); delay(100); } // end dispLine() void receiveEvent(int howMany) { // i2c event incoming requests if (isBusy==false) { int counter=0; s_clear(recCommand, sizeof(recCommand)); // sender command recAddress=0; // sender address recType=0; // sender cmd type if (howMany == 0) { // ignores empty requests isNetDataWaiting=false; } else { while (Wire.available()) { data[0]=Wire.read(); if (counter==0) { recType = data[0]; } // 1st char = cmd type if (counter==1) { recAddress = data[0]; } // 2nd char = sender address if (counter >1) { // other chars = command s_concat(recCommand, data, sizeof(recCommand)); } counter++; } isNetDataWaiting=true; } }//endisBusy } // end receiveEvent() void netWhoIsEvent() { // i2c event: 11 bytes = 1=char(5) ENQ, 2-9=netMyNAME 10=netMyPx 11=netMyAddress if (isBusy==false) { char ccommand[65]=""; char park[3]=""; s_assign(ccommand, _ENQ, sizeof(ccommand)); // ENQ s_concat(ccommand, netMyNAME, sizeof(ccommand)); // netMyNAME s_concat(ccommand, itoa(netMyPx,park,10), sizeof(ccommand)); // netMyPx s_concat(ccommand, _MYADDR, sizeof(ccommand)); // netMyAddress Wire.write(ccommand); } } // end netWhoIsEvent() void txBackToSender() { // tx back to sender char ccommand[65]=""; s_assign(ccommand, _ACK, sizeof(ccommand)); // ACK=answer data s_concat(ccommand, _MYADDR, sizeof(ccommand)); // netMyAddress s_concat(ccommand, lineString, sizeof(ccommand)); // data Wire.beginTransmission (recAddress); Wire.write (ccommand); Wire.endTransmission (true); } // end txBackToSender() //------------------------------------------------------------------------- CLEAR void s_clear(char *dest_source_string, const int dest_sizeof) { // fills-up with NUL=chr(0) memset(dest_source_string, 0, dest_sizeof); } // end s_clear() //------------------------------------------------------------------------- ASSIGN bool s_assign(char *dest_string, const char *source_string, const int dest_sizeof) { // copies source to dest int _LenS=strlen(source_string); // how many bytes if (_LenS > (dest_sizeof - 1)) { return true; // err 1 } else { strcpy(dest_string, source_string); return false; // ok 0 } } // end s_assign() //------------------------------------------------------------------------- SUBSTRING bool s_substring(char *dest_string, const int dest_sizeof, const char *source_string, const int source_sizeof, const int source_from, const int source_to) { // copies source(from, to) to dest if ((source_from < 0) || (source_to < source_from) || ((source_to - source_from + 1) > (dest_sizeof - 1)) || (source_to >= (source_sizeof-1)) || ((source_to - source_from + 1) > (strlen(source_string)))) { dest_string[0]=0; // NUL return true; // err 1 } else { int _Count=0; for (int i=source_from;i<(source_to+1);i++) { dest_string[_Count]=source_string[i]; _Count++; } dest_string[_Count]=0; // ends with NUL return false; // ok 0 } } // end s_substring() //------------------------------------------------------------------------- CONCAT bool s_concat(char *dest_string, const char *source_string, const int dest_sizeof) { // append source to dest int _LenS=strlen(source_string); // how many bytes source int _LenD=strlen(dest_string); // how many bytes dest if ((_LenS + _LenD) > (dest_sizeof - 1)) { return true; // err 1 } else { strcat(dest_string, source_string); return false; // ok 0 } } // end s_concat() //------------------------------------------------------------------------- COMPARE bool s_compare(const char *dest_string, const char *source_string) { // compares source with dest int _LenS=strlen(source_string); // how many bytes source int _LenD=strlen(dest_string); // how many bytes dest if (_LenS != _LenD) { // different length return true; // are different 1 } else { if (strcmp(dest_string, source_string) == 0) { return false; // are the same 0 } else { return true; // are different 1 } } } // end s_compare() //------------------------------------------------------------------------- FREEMEM int s_freemem() { extern int __heap_start,*__brkval; int v; return (int)&v -