Introduction
In today's changing industrial automation landscape, there is a growing interest in incorporating new web technologies into traditional programmable logic controllers. The potential of using frameworks, adding libraries for additional capabilities and integrating databases into these systems presents exciting possibilities for improving functionality and efficiency.
In this two-part blog series, we will explore the practical applications and benefits of leveraging these web technologies in industrial PLCs, opening up new avenues for innovation and industrial process improvement.
What are web frameworks?
Web frameworks play a key role in the development of front-end and back-end components of web applications. These frameworks provide developers with a structured and efficient approach to creating robust and scalable software solutions, whether they focus on the user-oriented front-end or the server-side back-end.
Front-end frameworks, such as Svelte and React, offer developers ways to create interactive and visually appealing user interfaces. These frameworks offer a collection of pre-configured tools, libraries and components that simplify the process of designing responsive layouts, managing complex application logic and handling user interactions. Thanks to their declarative syntax and efficient rendering techniques, front-end frameworks allow developers to create complex web applications more efficiently and easily maintainable.
On the other hand, back-end frameworks, such as SvelteKit, Express.js or Django, provide a structured environment for developing the server-side logic of web applications. These frameworks take care of tasks such as routing, database interactions, authentication and API integration, allowing developers to focus on implementing business logic and ensuring secure and efficient communication between the front-end and the server.
In this blog, you will learn how to install the Svelte front-end framework on one of Industrial Shields ESP32 PLCs. By integrating Svelte into this PLC, you can leverage the power of this innovative framework to develop intuitive and efficient user interfaces tailored to your specific industrial needs.
What are Svelte and Sveltekit?
What is Svelte?
Svelte takes a unique approach by using a compiler, allowing developers to create incredibly compact components that do minimal work in the browser environment. This compilation process optimises the resulting JavaScript code, resulting in lightning-fast load times and an exceptional user experience.
It also adds a reactive approach to UI development, which involves building interfaces that can automatically update themselves in response to changes in the underlying data or user interactions. Traditionally, web applications relied on manual manipulation of the DOM (and indirect manipulation of the virtual DOM in frameworks such as React) to update the UI, often resulting in complex and error-prone code.
Svelte's reactive approach, driven by its compilation process, allows developers to create UI components that update automatically when their underlying state changes. This means that developers can simply update the state of a variable, and Svelte takes care of efficiently propagating those changes to the UI. The result is a high-performance application that responds seamlessly to user interactions and data updates, providing a smooth and engaging user experience.
What is SvelteKit?
SvelteKit is an end-to-end solution built on top of Svelte. The framework includes a robust routing system, which allows seamless navigation between different pages of the application, etc. In addition, SvelteKit supports server-side rendering (SSR), which allows web pages to be pre-rendered on the server and delivered to the browser as fully rendered HTML, improving initial page load performance.
In addition, SvelteKit offers serverless deployment capabilities, allowing developers to deploy their applications on serverless platforms such as Netlify or Vercel with ease. Serverless deployment eliminates the need to manage servers and infrastructure, simplifying the deployment process and reducing maintenance overhead. However, it is important to note that serverless deployment comes with certain advantages and disadvantages, such as the inability to use server-side rendering or execute server-side JavaScript code.
How to use SvelteKit in Industrial Shields PLCs
When it comes to integrating web technologies such as Svelte and SvelteKit into our industrial PLCs, it is essential to take into account the limitations and capabilities of the hardware and software environment. For example, PLCs such as the M-Duino and ESP32 families will not have the ability to run servers due to the lack of compatible software to run a Node.js back-end. Instead, we will have to use serverless deployment.
Note that this is not a limitation unique to Svelte, it applies to any web framework (React, Vue.js...).
SvelteKit offers an alternative approach called "static adapter" that should be used in situations where you need serverless deployment. The static adapter, which is a preset for the Svelte compiler, allows for the pre-rendering of all application pages during the compilation process. This means that the resulting web application consists of static HTML, CSS and JavaScript files that can be deployed directly to the PLC without the need for server-side processing.
By leveraging the static adapter, we can still gain benefits such as improved initial page load performance and search engine optimisation. Pre-rendered static files ensure that content is available immediately, reducing the time required for the PLC to render the initial interface, allowing developers to create efficient and interactive web applications that match the capabilities of our PLCs.
How to set up an SvelteKit app
Requirements
First, you will need to install Node.js and npm. Node.js is a JavaScript runtime environment based on Chrome's V8 engine. It allows developers to run JavaScript code outside of a web browser, enabling server-side JavaScript execution. Node.js offers a non-blocking, event-driven I/O model that makes it lightweight, efficient and suitable for building scalable network applications. npm is a command-line tool that comes bundled with Node.js. It takes care of managing and installing the packages and dependencies needed for your project.
If you are on Windows, you can follow Microsoft's instructions on their official website. If you are on Mac or Linux, we recommend you to use nvm, a version manager for Node.js that allows you to easily switch between different versions of Node.js. You can install nvm by following the instructions provided at heynode.com.
Start an empty SvelteKit app
With Node.js and npm installed, you will then need to execute the following commands:
npm create svelte@latest my-app
cd my-app
npm install
npm run dev
With these commands, you will create a folder named my-app, install the dependencies and run a Vite instance (a development server to test the web app before sending it to the PLC). The first command will ask you the following:
Which Svelte app
template?
We recommend the Skeleton project, as the demo app has to be modified to in order to deploy serverlessly.
Add type checking with TypeScript?
You can choose to not do so. TypeScript is just a JavaScript dialect that includes type checking. In this tutorial we will use TypeScript syntax.
Select
additional options
We will not use any for this example app.
Once the Vite instance is up, you could then access the test app through http://localhost:5173.
Setting up the static adapter
The static adapter in SvelteKit prerenders your entire website as a collection of static files, which can then be easily deployed at the PLCs. Setting up the static adapter involves a few simple steps.
First, you need to install the static adapter package using npm, the package manager for Node.js. Open your command-line interface and execute the following command:
npm install -D @sveltejs/adapter-static
Next, you need to configure SvelteKit to use the static adapter. To do this, you will need to modify the svelte.config.js file in the root directory of your project. If the file doesn't exist, you can create it. Open the svelte.config.js file and add the following code:
import adapter from '@sveltejs/adapter-static';
export default {
kit: {
adapter: adapter({
pages: 'build',
assets: 'build',
fallback: undefined,
precompress: false,
strict: true
})
}
};
Once you have made these changes, the static adapter is configured to generate the static files during the build process. When you build your SvelteKit application with npm run build, the static files will be created in the "build" directory, ready to deploy at the PLC.
Example app
Modified +page.svelte file
<scrip>
let number = 0;
function increment() {
if (number < 10) {
number += 1;
}
}
function decrement() {
if (number > 0) {
number -= 1;
}
}
$: double_number = number*2;
</scrip>
<style>
.button {
width: 48px;
height: 48px;
}
</style>
<main>
<h1>Welcome to SvelteKit</h1>
<p>This web shows a number that can be incremented or decremented with buttons.</p>
<p>This number can only be between 0 and 10.</p>
<p>Number = {number}</p>
<p>Number*2 = {double_number}</p>
<div style="display:flex">
<button on:click={increment} class="button">+</button>
<div style="width:16px"></div>
<button on:click={decrement} class="button">-</button>
</div>
</main>
In a typical UI approach, when working with variables that need to be updated based on user interactions, we would have to handle the update logic manually. This often involves listening to events or implementing other mechanisms to track and update the variables separately. With the reactive approach, Svelte takes care of this and you only have to declare the multiplied variable with a reactive statement. In this example, by using the reactive statement "$: double_number = number * 2", Svelte creates the code behind the scenes that ensures "double_number" is always the double of "number". This reactive approach simplifies the code and eliminates the need for explicit event handling or separate update logic.
Sending the web app inside the PLC
We already have an existing tutorial that explains how to store files in the flash memory of an ESP32 to serve a web page. However, there are situations where using the SD card becomes more advantageous, like if we are serving larger applications or when the flash memory has limited space remaining.
To begin with, ensure that you have built the latest version of your web app using the command npm run build. This step is necessary to have the most recent changes available for copying. Next, you will need to insert the SD card into your PC. Format the SD card to the FAT32 file system. Once formatted, create a folder on the SD card named "www". This folder will serve as the root directory for hosting your web content. Now, navigate to the "build" directory within your Svelte project folder. Copy all the contents of this "build" directory and paste them into the "www" folder on your SD card.
Set-up the web server within the PLC
The most suitable library for this example is the ESP32AsyncWebSrv, an asynchronous HTTP and WebSocket Server Library for ESP32. This library is utilized due to its asynchronous approach and built-in extensions for handling direct reading from the SPIFFS or SD, simplifying the process of serving web content from the SD card. But there are some considerations that we have keep in mind.
Firstly, the SD library that we normally use doesn't support the FAT32 long filenames extension, which is a problem because most of the generated files by the Svelte framework surpasses the filename limits. A good alternative is the SdFat library as it provides the required support, and it's compatible with our PLCs.
However, to integrate the SdFat library with the web server, the FS object used by the web server needs to be adapted to work with the SdFat library. Fortunately, it exists an unofficial adaptation of the SdFat object that is compatible with both the PLCs and the Web server library. Now it is possible for the ESPAsyncWebSrv library to directly use the SD card through the SdFat library.
esp32-server.cpp
#include <AsyncTCP.h>
#include <ESPAsyncWebSrv.h>
AsyncWebServer server(80);
const char* ssid = "ISHIELDS";
const char* password = "boot2015SHIELDS*";
#include <SdFat.h>
#include <sdios.h>
#include "FS_SdFat_Wrapper.h"
#define SD_CONFIG SdSpiConfig(13, SHARED_SPI, SD_SCK_MHZ(16))
SdFat32 sd;
fs::FS sdFS = fs::FS(fs::FSImplPtr(new SdFat32FSImpl(sd)));
void notFound(AsyncWebServerRequest *request) {
request->send(404, "text/plain", "Not found");
}
void print_sd_errors(void) {
Serial.println(F("Code,Symbol - failed operation"));
for (uint8_t code = 0; code <= SD_CARD_ERROR_UNKNOWN; code++) {
Serial.print(code < 16 ? "0X0" : "0X");
Serial.print(code, HEX);
Serial.print(",");
printSdErrorSymbol(&Serial, code);
Serial.print(" - ");
printSdErrorText(&Serial, code);
Serial.println();
}
}
void setup() {
Serial.begin(115200);
if (!sd.begin(SD_CONFIG)) {
print_sd_errors();
sd.initErrorHalt();
}
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
if (WiFi.waitForConnectResult() != WL_CONNECTED) {
Serial.printf("WiFi Failed!\n");
return;
}
Serial.print("IP Address: ");
Serial.println(WiFi.localIP());
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->redirect("/index.html");
});
server.serveStatic("/", sdFS, "/www");
server.onNotFound(notFound);
server.begin();
}
void loop() {
yield();
if ((WiFi.status() != WL_CONNECTED)) {
Serial.println("Reconnecting to WiFi...");
WiFi.disconnect();
WiFi.reconnect();
}
}
FS_SdFat_Wrapper.h
#include <FS.h>
#include <FSImpl.h>
#include <SdFat.h>
// cfr https://en.cppreference.com/w/c/io/fopen + guesses
inline oflag_t _convert_access_mode_to_flag(const char* mode, const bool create = false) {
int mode_chars = strlen(mode);
if (mode_chars==0) return O_RDONLY;
if (mode_chars==1) {
if (mode[0]=='r') return O_RDONLY;
if (mode[0]=='w') return O_WRONLY | create ? O_CREAT : 0;
if (mode[0]=='a') return O_APPEND | create ? O_CREAT : 0;
}
if (mode_chars==2) {
if (mode[1] == '+') {
if (mode[0] == 'r') return O_RDWR;
if (mode[0] == 'w') return O_RDWR | O_CREAT;
if (mode[0] == 'a') return O_RDWR | O_APPEND | O_CREAT;
}
}
return O_RDONLY;
}
class SdFatFile32Impl : public fs::FileImpl
{
private:
mutable File32 _file;
public:
SdFatFile32Impl(File32 file) : _file(file) {}
virtual ~SdFatFile32Impl() { }
virtual size_t write(const uint8_t *buf, size_t size) {
return _file.write(buf, size);
}
virtual size_t read(uint8_t* buf, size_t size) {
return _file.read(buf, size);
}
virtual void flush() {
return _file.flush();
}
virtual bool seek(uint32_t pos, fs::SeekMode mode) {
if (mode == fs::SeekMode::SeekSet) {
return _file.seek(pos);
} else if (mode == fs::SeekMode::SeekCur) {
return _file.seek(position()+ pos);
} else if (mode == fs::SeekMode::SeekEnd) {
return _file.seek(size()-pos);
}
return false;
}
virtual size_t position() const {
return _file.curPosition();
}
virtual size_t size() const {
return _file.size();
}
virtual bool setBufferSize(size_t size) {
// don't know how to implement...
return false;
}
virtual void close() {
_file.close();
}
virtual time_t getLastWrite() {
// didn't want to implement ...
return 0;
}
virtual const char* path() const {
// didn't want to implement ...
return nullptr;
}
virtual const char* name() const {
// static, so if one asks the name of another file the same buffer will be used.
// so we assume here the name ptr is not kept. (anyhow how would it be dereferenced and then cleaned...)
static char _name[256];
_file.getName(_name, sizeof(_name));
return _name;
}
virtual boolean isDirectory(void) {
return _file.isDirectory();
}
virtual fs::FileImplPtr openNextFile(const char* mode) {
return std::make_shared<SdFatFile32Impl>(_file.openNextFile(_convert_access_mode_to_flag(mode)));
}
virtual boolean seekDir(long position) {
return _file.seek(position);
}
virtual String getNextFileName(void) {
return String("no way");
}
virtual void rewindDirectory(void) {
return;
}
virtual operator bool() {
return _file.operator bool();
}
};
class SdFat32FSImpl : public fs::FSImpl
{
SdFat32& sd;
public:
SdFat32FSImpl(SdFat32& sd) : sd(sd)
{
}
virtual ~SdFat32FSImpl() {}
virtual fs::FileImplPtr open(const char* path, const char* mode, const bool create) {
return std::make_shared<SdFatFile32Impl>(sd.open(path, _convert_access_mode_to_flag(mode, create)));
}
virtual bool exists(const char* path) {
return sd.exists(path);
}
virtual bool rename(const char* pathFrom, const char* pathTo) {
return sd.rename(pathFrom, pathTo);
}
virtual bool remove(const char* path) {
return sd.remove(path);
}
virtual bool mkdir(const char *path) {
return sd.mkdir(path);
}
virtual bool rmdir(const char *path) {
return sd.rmdir(path);
}
};
extern fs::FS SdFat32Fs;
Conclusion
In conclusion, the integration of web technologies, such as Svelte and SvelteKit, into Industrial Shield PLCs offers promising possibilities for improving industrial automation. The use of web frameworks facilitates the development of complex web applications with intuitive and visually appealing user interfaces. Svelte's unique approach, using a compiler and reactive user interface development paradigm, results in lightning-fast load times and a seamless user experience.
Despite the limitations of some PLC equipment, such as the inability to run servers, SvelteKit's static adapter allows serverless deployment, enabling pre-rendering of web pages and optimising initial page load performance. By following the necessary steps to configure SvelteKit applications and using the SdFat library for seamless web server integration, it is feasible to deploy web applications directly on PLC SD cards, overcoming space limitations and ensuring efficient web hosting.
Overall, the adoption of web technologies in Industrial Shields PLCs opens up new avenues for innovation, efficiency and process improvement, propelling industry into the era of Industry 4.0 and the Internet of Things.
Use of web technologies in Industrial Shields PLCs