Embedded JavaScript engine for C/C++

Overview

mJS: Restricted JavaScript engine

License

Overview

mJS is designed for microcontrollers with limited resources. Main design goals are: small footprint and simple C/C++ interoperability. mJS implements a strict subset of ES6 (JavaScript version 6):

  • Any valid mJS code is a valid ES6 code.
  • Any valid ES6 code is not necessarily a valid mJS code.

On 32-bit ARM mJS engine takes about 50k of flash memory, and less than 1k of RAM (see intro article). mJS is part of MongooseOS, where it enables scripting for IoT devices.

Restrictions

  • No standard library. No String, Number, RegExp, Date, Function, etc.
  • JSON.parse() and JSON.stringify() are available.
  • No closures, only lexical scoping (i.e. nested functions are allowed).
  • No exceptions.
  • No new. In order to create an object with a custom prototype, use Object.create(), which is available.
  • Strict mode only.
  • No var, only let.
  • No for..of, =>, destructors, generators, proxies, promises.
  • No getters, setters, valueOf, prototypes, classes, template strings.
  • No == or !=, only === and !==.
  • mJS strings are byte strings, not Unicode strings: 'ы'.length === 2, 'ы'[0] === '\xd1', 'ы'[1] === '\x8b'. mJS string can represent any binary data chunk.

Built-in API

print(arg1, arg2, ...);
Print arguments to stdout, separated by space.
load('file.js', obj);
Execute file file.js. obj paramenter is optional. obj is a global namespace object. If not specified, a current global namespace is passed to the script, which allows file.js to modify the current namespace.
die(message);
Exit interpreter with the given error message
let value = JSON.parse(str);
Parse JSON string and return parsed value.
let str = JSON.stringify(value);
Get string representation of the mJS value.
let proto = {foo: 1}; let o = Object.create(proto);
Create an object with the provided prototype.
'some_string'.slice(start, end);
Return a substring between two indices. Example: 'abcdef'.slice(1,3) === 'bc';
'abc'.at(0);
Return numeric byte value at given string index. Example: 'abc'.at(0) === 0x61;
'abc'.indexOf(substr[, fromIndex]);
Return index of first occurence of substr within the string or `-1` if not found. 'abc'.indexOf('bc') === 1;
chr(n);
Return 1-byte string whose ASCII code is the integer `n`. If `n` is not numeric or outside of `0-255` range, `null` is returned. Example: chr(0x61) === 'a';
let a = [1,2,3,4,5]; a.splice(start, deleteCount, ...);
Change the contents of an array by removing existing elements and/or adding new elements. Example: let a = [1,2,3,4,5]; a.splice(1, 2, 100, 101, 102); a === [1,100,101,102,4,5];
let s = mkstr(ptrVar, length);
Create a string backed by a C memory chunk. A string s starts at memory location ptrVar, and is length bytes long.
let s = mkstr(ptrVar, offset, length, copy = false);
Like `mkstr(ptrVar, length)`, but string s starts at memory location ptrVar + offset, and the caller can specify whether the string needs to be copied to the internal mjs buffer. By default it's not copied.
let f = ffi('int foo(int)');
Import C function into mJS. See next section.
gc(full);
Perform garbage collection. If `full` is `true`, reclaim RAM to OS.

C/C++ interoperability

mJS requires no glue code. The mJS's Foreign Function Interface (FFI) allows the user to call an existing C function with an arbitrary signature. Currently mJS provides a simple implementation of the FFI trampoline that supports up to 6 32-bit arguments, or up to 2 64-bit arguments:

let floor = ffi('double floor(double)');
print(floor(1.23456));

Function arguments should be simple: only int, double, char *, void * are supported. Use char * for NUL-terminated C strings, void * for any other pointers. In order to import more complex functions (e.g. the ones that use structures as arguments), write wrappers.

Callbacks

Callbacks are implemented similarly. Consider that you have a C function that takes a callback and user data void * pointer, which should be marked as userdata in the signature:

void timer(int seconds, void (*callback)(int, void *), void *user_data);

This is how to make an mJS callback - note the usage of userdata:

let Timer = {
  set: ffi('void timer(int, void (*)(int, userdata), userdata)')
};

Timer.set(200, function(t) {
  print('Time now: ', t);
}, null);

Symbol resolver

In order to make FFI work, mJS must be able to get the address of a C function by its name. On POSIX systems, dlsym() API can do that. On Windows, GetProcAddress(). On embedded systems, a system resolver should be either manually written, or be implemented with some aid from a firmware linker script. mJS resolver uses dlsym-compatible signature.

Converting structs to objects

mJS provides a helper to facilitate coversion of C structs to JS objects. The functions is called s2o and takes two parameters: foreign pointer to the struct and foreign pointer to the struct's descriptor which specifies names and offsets of the struct's members. Here's an simple example:

C/C++ side code:

#include "mjs.h"

struct my_struct {
  int a;
  const char *b;
  double c;
  struct mg_str d;
  struct mg_str *e;
  float f;
  bool g;
};

static const struct mjs_c_struct_member my_struct_descr[] = {
  {"a", offsetof(struct my_struct, a), MJS_STRUCT_FIELD_TYPE_INT, NULL},
  {"b", offsetof(struct my_struct, b), MJS_STRUCT_FIELD_TYPE_CHAR_PTR, NULL},
  {"c", offsetof(struct my_struct, c), MJS_STRUCT_FIELD_TYPE_DOUBLE, NULL},
  {"d", offsetof(struct my_struct, d), MJS_STRUCT_FIELD_TYPE_MG_STR, NULL},
  {"e", offsetof(struct my_struct, e), MJS_STRUCT_FIELD_TYPE_MG_STR_PTR, NULL},
  {"f", offsetof(struct my_struct, f), MJS_STRUCT_FIELD_TYPE_FLOAT, NULL},
  {"g", offsetof(struct my_struct, g), MJS_STRUCT_FIELD_TYPE_BOOL, NULL},
  {NULL, 0, MJS_STRUCT_FIELD_TYPE_INVALID, NULL},
};

const struct mjs_c_struct_member *get_my_struct_descr(void) {
  return my_struct_descr;
};

JS side code:

// Assuming `s` is a foreign pointer to an instance of `my_struct`, obtained elsewhere.
let sd = ffi('void *get_my_struct_descr(void)')();
let o = s2o(s, sd);
print(o.a, o.b);

Nested structs are also supported - use MJS_STRUCT_FIELD_TYPE_STRUCT field type and provide pointer to the definition:

struct my_struct2 {
  int8_t i8;
  int16_t i16;
  uint8_t u8;
  uint16_t u16;
};

static const struct mjs_c_struct_member my_struct2_descr[] = {
  {"i8", offsetof(struct my_struct2, i8), MJS_STRUCT_FIELD_TYPE_INT8, NULL},
  {"i16", offsetof(struct my_struct2, i16), MJS_STRUCT_FIELD_TYPE_INT16, NULL},
  {"u8", offsetof(struct my_struct2, u8), MJS_STRUCT_FIELD_TYPE_UINT8, NULL},
  {"u16", offsetof(struct my_struct2, u16), MJS_STRUCT_FIELD_TYPE_UINT16, NULL},
  {NULL, 0, MJS_STRUCT_FIELD_TYPE_INVALID, NULL},
};

struct my_struct {
  struct my_struct2 s;
  struct my_struct2 *sp;
};

static const struct mjs_c_struct_member my_struct_descr[] = {
  {"s", offsetof(struct my_struct, s), MJS_STRUCT_FIELD_TYPE_STRUCT, my_struct2_descr},
  {"sp", offsetof(struct my_struct, sp), MJS_STRUCT_FIELD_TYPE_STRUCT_PTR, my_struct2_descr},
  {NULL, 0, MJS_STRUCT_FIELD_TYPE_INVALID, NULL},
};

For complicated cases, a custom conversion function can be invoked that returns value:

mjs_val_t custom_value_func(struct mjs *mjs, void *ap) {
  /* Do something with ap, construct and return mjs_val_t */
}

static const struct mjs_c_struct_member my_struct_descr[] = {
  ...
  {"x", offsetof(struct my_struct, x), MJS_STRUCT_FIELD_TYPE_CUSTOM, custom_value_func},
  ...
};

Complete embedding example

We export C function foo to the JS environment and call it from the JS.

#include "strings.h"
#include "mjs.h"

void foo(int x) {
  printf("Hello %d!\n", x);
}

void *my_dlsym(void *handle, const char *name) {
  if (strcmp(name, "foo") == 0) return foo;
  return NULL;
}

int main(void) {
  struct mjs *mjs = mjs_create();
  mjs_set_ffi_resolver(mjs, my_dlsym);
  mjs_exec(mjs, "let f = ffi('void foo(int)'); f(1234)", NULL);
  return 0;
}

Compile & run:

$ cc main.c mjs.c -o /tmp/x && /tmp/x
Hello 1234!

Build stand-alone mJS binary

Build:

$ make

Use as a simple calculator:

$ ./build/mjs -e '1 + 2 * 3'
7

FFI standard C functions:

$ ./build/mjs -e 'ffi("double sin(double)")(1.23)'
0.942489

View generated bytecode:

$ ./build/mjs -l 3 -e '2 + 2'
------- MJS VM DUMP BEGIN
    DATA_STACK (0 elems):
    CALL_STACK (0 elems):
        SCOPES (1 elems):  [<object>]
  LOOP_OFFSETS (0 elems):
  CODE:
  0   BCODE_HDR [<stdin>] size:28
  21  PUSH_INT  2
  23  PUSH_INT  2
  25  EXPR      +
  27  EXIT
  28  NOP
------- MJS VM DUMP END
4

The stand-alone binary uses dlsym() symbol resolver, that's why ffi("double sin(double)")(1.23) works.

Licensing

mJS is released under commercial and GNU GPL v.2 open source licenses.

Commercial Projects: once your project becomes commercialised, GPLv2 licensing dictates that you need to either open your source fully or purchase a commercial license. Cesanta offer full, royalty-free commercial licenses without any GPL restrictions. If your needs require a custom license, we’d be happy to work on a solution with you. Contact us for pricing

Prototyping: While your project is still in prototyping stage and not for sale, you can use MJS’s open source code without license restrictions.

Issues
  • SEGV src/mjs_json.c:238 in to_json_or_debug

    SEGV src/mjs_json.c:238 in to_json_or_debug

    mJS revision

    Commit: b1b6eac

    Build platform

    Ubuntu 18.04.5 LTS (Linux 5.4.0-44-generic x86_64)

    Build steps
    vim Makefile
    DOCKER_GCC=gcc
    $(DOCKER_GCC) $(CFLAGS) $(TOP_MJS_SOURCES) $(TOP_COMMON_SOURCES) -o $(PROG)
    # save the makefile then make
    make
    
    Test case
    poc.js
    
    
    (JSON.stringify([1, 2, 3]))(((JSON.parse-6.54321e3)-6.54321e2)(JSON.stringify([1, 2, 3])));
    

    Execution steps & Output
    $ ./mjs/build/mjs poc.js
    ASAN:DEADLYSIGNAL
    =================================================================
    ==37535==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000100 (pc 0x55f1bcaef797 bp 0x000000000089 sp 0x7ffc2c9cf150 T0)
    ==37535==The signal is caused by a WRITE memory access.
    ==37535==Hint: address points to the zero page.
        #0 0x55f1bcaef796 in to_json_or_debug src/mjs_json.c:238
        #1 0x55f1bcaef796 in mjs_json_stringify src/mjs_json.c:267
        #2 0x55f1bcaef796 in mjs_op_json_stringify src/mjs_json.c:494
    
    AddressSanitizer can not provide additional info.
    SUMMARY: AddressSanitizer: SEGV src/mjs_json.c:238 in to_json_or_debug
    ==37535==ABORTING
    

    Credits: Found by OWL337 team.

    opened by hope-fly 0
  • SEGV src/mjs_json.c:323 in free_json_frame

    SEGV src/mjs_json.c:323 in free_json_frame

    mJS revision

    Commit: b1b6eac

    Build platform

    Ubuntu 18.04.5 LTS (Linux 5.4.0-44-generic x86_64)

    Build steps
    vim Makefile
    DOCKER_GCC=gcc
    $(DOCKER_GCC) $(CFLAGS) $(TOP_MJS_SOURCES) $(TOP_COMMON_SOURCES) -o $(PROG)
    # save the makefile then make
    make
    
    Test case
    poc.js
    
    
    (JSON.stringify([1, 2, 3]))((JSON.stringify-6.34321e2)(JSON.stringify([1, 2, 3])));
    

    Execution steps & Output
    $ ./mjs/build/mjs poc.js
    > $ Gmjs poc.js
    ASAN:DEADLYSIGNAL
    =================================================================
    ======ERROR: AddressSanitizer: SEGV on unknown address 0x561b4e461e9c (pc 0x561b4e461ed5 bp 0x000000000080 sp 0x7ffdacf6fb18 T0)
    ======The signal is caused by a WRITE memory access.
        #0 0x561b4e461ed4 in free_json_frame src/mjs_json.c:323
        #1 0x561b4e461ed4 in mjs_json_parse src/mjs_json.c:476
        #2 0x7ffdacf6fc0f  (<unknown module>)
    
    AddressSanitizer can not provide additional info.
    SUMMARY: AddressSanitizer: SEGV src/mjs_json.c:323 in free_json_frame
    ======ABORTING
    

    Credits: Found by OWL337 team.

    opened by hope-fly 0
  • SEGV src/mjs_json.c:273 in mjs_json_stringify

    SEGV src/mjs_json.c:273 in mjs_json_stringify

    mJS revision

    Commit: b1b6eac

    Build platform

    Ubuntu 18.04.5 LTS (Linux 5.4.0-44-generic x86_64)

    Build steps
    vim Makefile
    DOCKER_GCC=gcc
    $(DOCKER_GCC) $(CFLAGS) $(TOP_MJS_SOURCES) $(TOP_COMMON_SOURCES) -o $(PROG)
    # save the makefile then make
    make
    
    Test case
    poc.js
    
    
    (JSON.stringify([1, 2, 3]))((JSON.parse-6.54*21e2)(JSON.stringify([1, 2, 3])));
    

    Execution steps & Output
    $ ./mjs/build/mjs poc.js
    ASAN:DEADLYSIGNAL
    =================================================================
    ==61059==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x56205ea42cf1 bp 0x00000000007c sp 0x7ffdf0132cb8 T0)
    ==61059==The signal is caused by a READ memory access.
    ==61059==Hint: address points to the zero page.
        #0 0x56205ea42cf0 in mjs_json_stringify src/mjs_json.c:273
        #1 0x56205ea42cf0 in mjs_json_stringify src/mjs_json.c:272
        #2 0x56205ea42cf0 in mjs_json_stringify src/mjs_json.c:272
        #3 0x56205ea42cf0 in mjs_op_json_stringify src/mjs_json.c:494
        #4 0x56205ead6955 in cs_varint_decode src/common/cs_varint.c:65
        #5 0x56205eaa1a05 in mjs_strcmp src/mjs_string.c:228
        #6 0x60b000000082  (<unknown module>)
    
    AddressSanitizer can not provide additional info.
    SUMMARY: AddressSanitizer: SEGV src/mjs_json.c:273 in mjs_json_stringify
    

    Credits: Found by OWL337 team.

    opened by hope-fly 0
  • SEGV src/mjs_bcode.c:15 in add_lineno_map_item

    SEGV src/mjs_bcode.c:15 in add_lineno_map_item

    mJS revision

    Commit: b1b6eac

    Build platform

    Ubuntu 18.04.5 LTS (Linux 5.4.0-44-generic x86_64)

    Build steps
    vim Makefile
    DOCKER_GCC=gcc
    $(DOCKER_GCC) $(CFLAGS) $(TOP_MJS_SOURCES) $(TOP_COMMON_SOURCES) -o $(PROG)
    # save the makefile then make
    make
    
    Test case
    poc.js
    
    
    (JSON.stringify([1, 2, 3]))(((print-6.32*(823))-6.32*21e2)(JSON.parse(JSON.stringify([(0)]))));J
    

    Execution steps & Output
    $ ./mjs/build/mjs poc.js
    ASAN:DEADLYSIGNAL
    =================================================================
    ==82386==ERROR: AddressSanitizer: SEGV on unknown address 0x0000000000e4 (pc 0x55a4aeab6fd4 bp 0x00000000008c sp 0x7ffc17a28520 T0)
    ==82386==The signal is caused by a READ memory access.
    ==82386==Hint: address points to the zero page.
        #0 0x55a4aeab6fd3 in add_lineno_map_item src/mjs_bcode.c:15
        #1 0x55a4aeab6fd3 in emit_int src/mjs_bcode.c:47
        #2 0x55a4aeb600dd in parse_comparison src/mjs_parser.c:431
        #3 0x55a4aeb600dd in parse_equality src/mjs_parser.c:435
        #4 0x55a4aeb65d0b in parse_bitwise_and src/mjs_parser.c:440
        #5 0x55a4aeb65d0b in parse_bitwise_xor src/mjs_parser.c:445
        #6 0x55a4aeb32177 in parse_bitwise_or src/mjs_parser.c:450
        #7 0x55a4aeb32177 in parse_logical_and src/mjs_parser.c:455
        #8 0x55a4aeb32177 in parse_logical_or src/mjs_parser.c:460
        #9 0x55a4aeb32177 in parse_ternary src/mjs_parser.c:465
        #10 0x55a4aeb32177 in parse_assignment src/mjs_parser.c:503
        #11 0x55a4aeb388d7 in parse_expr src/mjs_parser.c:507
        #12 0x55a4aeb388d7 in parse_statement src/mjs_parser.c:945
    
    AddressSanitizer can not provide additional info.
    SUMMARY: AddressSanitizer: SEGV src/mjs_bcode.c:15 in add_lineno_map_item
    ==82386==ABORTING
    

    Credits: Found by OWL337 team.

    opened by hope-fly 0
  • SEGV src/mjs_bcode.c:67 in mjs_bcode_insert_offset

    SEGV src/mjs_bcode.c:67 in mjs_bcode_insert_offset

    mJS revision

    Commit: b1b6eac

    Build platform

    Ubuntu 18.04.5 LTS (Linux 5.4.0-44-generic x86_64)

    Build steps
    vim Makefile
    DOCKER_GCC=gcc
    $(DOCKER_GCC) $(CFLAGS) $(TOP_MJS_SOURCES) $(TOP_COMMON_SOURCES) -o $(PROG)
    # save the makefile then make
    make
    
    Test case
    poc.js
    
    
    (JSON.stringify([1, 2, 3]))((load - 6.34 * 21e2)([([JSON.parse(JSON.stringify([(0)]))])]));
    

    Execution steps & Output
    $ ./mjs/build/mjs poc.js
    ASAN:DEADLYSIGNAL
    =================================================================
    ==88306==ERROR: AddressSanitizer: SEGV on unknown address 0x00000000008c (pc 0x558644d0edbc bp 0x000000000084 sp 0x7fffa37031f8 T0)
    ==88306==The signal is caused by a READ memory access.
    ==88306==Hint: address points to the zero page.
        #0 0x558644d0edbb in mjs_bcode_insert_offset src/mjs_bcode.c:67
        #1 0x7fffa3703a0f  (<unknown module>)
    
    AddressSanitizer can not provide additional info.
    SUMMARY: AddressSanitizer: SEGV src/mjs_bcode.c:67 in mjs_bcode_insert_offset
    ==88306==ABORTING
    
    opened by hope-fly 0
Releases(1.26)
  • 1.26(Feb 8, 2018)

    • Fix stack underflow on an empty block
    • Fix a bug which could cause string data corruption in certain circumstances
    • Fix a few modulo-related issues
    • Fix handling of invalid JSON strings
    • Fix JSON parsing which leads to string buffer reallocaction
    • Handle the wrong return expression properly
    Source code(tar.gz)
    Source code(zip)
Owner
Cesanta Software
Embedded Communication
Cesanta Software
Embedded Flutter runtime targeting Embedded Linux with Wayland

ivi-homescreen IVI Homescreen for Wayland Strongly Typed (C++) Lightweight Clang 11 Release Stripped = 151k GCC 9.3 Release Stripped = 168k Source run

null 159 Aug 8, 2022
Elk is a tiny embeddable JavaScript engine that implements a small but usable subset of ES6

Elk is a tiny embeddable JavaScript engine that implements a small but usable subset of ES6. It is designed for microcontroller development. Instead of writing firmware code in C/C++, Elk allows to develop in JavaScript. Another use case is providing customers with a secure, protected scripting environment for product customisation.

Cesanta Software 1.4k Jul 28, 2022
Android Bindings for QuickJS, A fine little javascript engine.

quickjs-android quickjs-android 是 QuickJS JavaScript 引擎的 Android 接口框架,整体基于面向对象设计,提供了自动GC功能,使用简单。armeabi-v7a 的大小仅 350KB,是 Google V8 不错的替代品,启动速度比 V8 快,内

Wiki 95 Jul 28, 2022
Header-only ECMAScript (JavaScript) compatible regular expression engine

SRELL (std::regex-like library) is a regular expression template library for C++ and has native support for UTF-8, UTF-16, and UTF-32. This is up-to-d

Dmitry Atamanov 4 Mar 11, 2022
LLVM bindings for Node.js/JavaScript/TypeScript

llvm-bindings LLVM bindings for Node.js/JavaScript/TypeScript Supported OS macOS Ubuntu Windows Supported LLVM methods listed in the TypeScript defini

ApsarasX 243 Aug 7, 2022
JavaScript runtime for Fastly [email protected]

Fastly [email protected] JS Runtime The JS Compute Runtime for Fastly's [email protected] platform provides the environment JavaScript is executed in when using

Fastly 83 Aug 3, 2022
A simple library that helps Android developers to execute JavaScript code from Android native side easily without using Webview.

AndroidJSModule A simple library that helps Android developers to execute JavaScript code from Android native side easily without using Webview. Insta

Hung Nguyen 5 May 24, 2022
A secure authentication system written in modern javascript and C++ 17

A fully fledged authentication system written in modern JavaScript and C++ 17. Written with the MEVN stack (MySQL, Express.js, Vue.js, Node.js) by a pro leet h4x0r.

vmexit 22 Jul 28, 2022
Convert Javascript/TypeScript to C

Convert Javascript/TypeScript to C

Andrei Markeev 1.1k Jul 27, 2022
StarkScript - or the Stark programming language - is a compiled C-based programming language that aims to offer the same usability as that of JavaScript's and TypeScript's

StarkScript StarkScript - or the Stark programming language - is a compiled C-based programming language that aims to offer the same usability as that

EnderCommunity 5 May 10, 2022
Window.js is an open-source Javascript runtime for desktop graphics programming.

Window.js Window.js is an open-source Javascript runtime for desktop graphics programming. It is documented at windowjs.org. Introduction Window.js pr

Window.js 2.2k Jul 31, 2022
A JavaScript interpreter from scratch, supporting ES5 syntax.

es A JavaScript interpreter from scratch, supporting ES5 syntax. Compile and Run Clone and compile: $ git clone [email protected]:zhuzilin/es.git $ cd es

Zilin Zhu 14 Jul 25, 2022
A complete Javascript environment for creating homebrew applications and games on PlayStation 2.

AthenaEnv is a project that seeks to facilitate and at the same time brings a complete kit for users to create homebrew software for PlayStation 2 using the JavaScript language.

Daniel Santos 17 May 18, 2022
Love 6's Regular Expression Engine. Support Concat/Select/Closure Basic function. Hope u can enjoy this tiny engine :)

Regex_Engine Love 6's Blog Website: https://love6.blog.csdn.net/ Love 6's Regular Expression Engine Hope u can love my tiny regex engine :) maybe a fe

Love6 2 May 24, 2022
Sword Engine is a fork of Psych Engine that plans on adding more features and quality of life improvements.

⚠️ WARNING: This README is currently incomplete, This warning will be removed once it's complete. Friday Night Funkin' - Sword Engine Sword Engine is

swordcube 7 Jul 9, 2022
Two Tribes Engine; the engine which we used to create Toki Tori 2+ and RIVE with

Two Tribes Engine We, Two Tribes, have used this engine for over a decade. It started with early development on the Nintendo DS and ultimately resulte

Two Tribes 117 Jun 21, 2022
OpenGL Template Engine - a C++ OpenGL graphics engine which aimed to be a simple startup template for 3D OpenGL projects.

OpenGL Template Engine is a C++ OpenGL graphics engine which aimed to be a simple startup template for 3D OpenGL projects. This is the template I personally use for my own projects and provides me with the general OpenGL 3D render setup with model import and UI.

Marcus Nesse Madland 2 May 16, 2022
Backtrace support for Rust `no_std` and embedded programs.

mini-backtrace This crate provides backtrace support for no_std and embedded programs. This is done through by compiling LLVM's libunwind with certain

Amanieu d'Antras 23 Jul 31, 2022
An embedded system for displaying current number of followers on bilibili. A reproduction of eInkBoard v1. 一个能显示哔哩哔哩账号实时粉丝数的嵌入式系统,eInkBoard v1 的复刻版。

eInkBoard v2 An embedded system for displaying current number of followers on bilibili. A reproduction of eInkBoard v1 (this page is in Chinese). 一个能显

Karbon Chen 4 Dec 6, 2021