Build a remote control car using Zephyr.js on Arduino 101

By haoxli
Use JavaScript to control an RC car!
Level: Intermediate
2h
Remote Control (RC) Cars are a childhood toy for many people. They are also a favorite project of microcontroller and electronic production enthusiasts. Frequently, web developers know little about microcontroller and embedded development. If we’d like to create such a RC car, do we have to start learning hardware programming from scratch? Thankfully, no.
zephyr
JavaScript
Robots

Introduction

Remote Control (RC) Cars are a childhood toy for many people. They are also a favorite project of microcontroller and electronic production enthusiasts. Frequently, web developers know little about microcontroller and embedded development.

If we’d like to create such a RC car, do we have to start learning hardware programming from scratch?

Thankfully, no.

In 2016, the Intel Open Source Technology Center (OTC) Web team started working with the JavaScript Runtime for Zephyr OS which called Zephyr.js (ZJS).

ZJS is based on JerryScript (a lightweight JavaScript engine) and Zephyr real-time operating system. It provides an IoT web runtime environment with a subset of Node.js APIs and JavaScript APIs that employ sensors, actuators, communications of devices, and allows web developers to save a lot of complex hardware programming. For those developers who know JavaScript, ZJS is easy to learn and develop applications quickly.

Let’s start to build a RC car with Bluetooth using the ZJS environment!

First of all, we need to prepare necessary devices and accessories. Below is a lsit of links to the items used in this article. You can use a different chassis if you want.

In addition to the above, you will need a computer capable of running the Zephyr development environment. You can see the official guide Classical 'Build and Flash' development for more information.

After assembling the car kit together, it should show up like Figure 3:

Tips: DO NOT connect the steering gear with the transmission rod when assembling. Because the initial angle of steering gear is uncertain, we need using Arduino 101 to calibrate the angle of steering gear to 90 degrees, this will ensure that the initial direction of front wheels are facing straight ahead.

Next, plug PM-R3 motor driver board to the corresponding pins of Arduino 101 as Figure 6:

Finally, secure Arduino 101 to RC car with screws, and battery pack with bundling belts, then connect the cables as Figure 7:

  • Wire the positive (red wire) of battery pack to the VIN on motor driver board through the switch button, the negative (back wire) to the GND.
  • Wire the motor to the A+ and A- on motor driver board.
  • Wire the red and brown wires of the steering gear to the power pins (5V and GND) on the motor driver board with male/male jumper wires, and the orange wire to the digital pin 3 (~3).

  1. Follow official guide Classical 'Build and Flash' development to initialize development environment.
  2. Build HelloWorld.js
copy
$
make JS=samples/HelloWorld.js

There is an image named zephyr.bin under zephyr.js/outdir/arduino_101/ folder when building successfully.

  1. Flash the image to device. Connect Arduino 101 to host machine with USB A/B cable. Press Master Reset button on Arduino 101 and after a few seconds type:
copy
$
make dfu
  1. Setup serial console. Without the serial console, we won't be able to see error messages and other output from ZJS application. On the USB to TTL serial cable, wire the black wire to GND on the Arduino 101 board, the green wire to digital pin 0 (RX0<-0), and the white wire to digital pin 1 (TX0->1). When plug this in, the device should show up as something such as /dev/ttyUSB0. Then connect to the device using the screen command:
copy
$
screen /dev/ttyUSB0 115200
  1. Press the Master Reset button to restart, following output will display in serial console:
Hello, ZJS world!

Congratulations!

Steering Gear Driving

Pulse Width Modulation (PWM) API of ZJS is used for driving steering gear to change steering angle, through transmission rod to control car’s direction. On Arduino 101 board, there are four pins that can be used as PWM output, which marked on the board by a ~ (tilde) symbol next to the pins, namely IO3, IO5, IO6, and IO9. Here we have wired steering gear with IO3 (~3).

The operating speed of steering gear (MG996R) is 0.13sec / 60 degrees (with 6.0V no load), but actually the time of operating 180 degrees need 2ms, around 0.11ms per degree. And the 0.5ms of pulse width make the steering gear turn to 0 degree. We use 90 degrees as an initial angle, so the front wheels are facing straight ahead. If we want to turn right for 30 degrees, the steering gear needs to be operated 60 degrees from 0 degree, in a similar way, 120 degrees for turning left 30 degrees. Example:

copy
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
// Pulse Width – Degree // 0.5ms-----------0c // 1.0ms-----------45c // 1.5ms-----------90c // 2.0ms-----------135c // 2.5ms-----------180c var pins = require("arduino101_pins"); var pwm = require("pwm"); // PWM Pin ~3 var steerPin = pwm.open({ channel: pins.IO3 }); // Clock Cycle(ms) var period = 20; steerPin.setPeriod(period); // Pulse Width/Degree: (2.5-0.5) / 180 (ms/c) var pw_angle = 0.011; // Front: 90 degree steerPin.setPulseWidth(1.5); // Right: 30 degree steerPin.setPulseWidth(0.5 + (90 - 30) * pw_angle); // Left: 30 degree steerPin.setPulseWidth(0.5 + (90 + 30) * pw_angle);

Tips: Correcting the angle of steering gear to 90 degrees is necessary before connecting it with the transmission rod when assembling. Use the pulse with 1.5ms in the sample, run it on Arduino 101, the angle will be corrected. Then fix the transmission rod with steering gear.

Motor Driving

The motor driving is also implemented by PWM API, but we don’t wire the motor to PWM pins, that’s because the motor drive board has implemented the pins of IO5 and IO6 by default for motor’s clockwise driving and counterclockwise driving. We control the motor power output by changing the duty cycle of PWM, the duty cycle of 0% for no power output and the duty cycle of 100% for full power output, and control forward or reverse by the duty cycle in IO6 and IO5. Example:

copy
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
var pins = require("arduino101_pins"); var pwm = require("pwm"); // IO6 for forward, IO5 for reverse var forwardPin = pwm.open({ channel: pins.IO6 }); var reversePin = pwm.open({ channel: pins.IO5 }); // Clock Cycle(ms) var period = 20; forwardPin.setPeriod(period); reversePin.setPeriod(period ); // Forward(half of power output): // duty cycle of IO6 is 50%, duty cycle of IO5 is 0% forwardPin.setPulseWidth(period*0.5); reversePin.setPulseWidth(0); // Reverse(half of power output): // duty cycle of IO6 is 0%, duty cycle of IO5 is 50% forwardPin.setPulseWidth(0); reversePin.setPulseWidth(period*0.5); // Brake(motor locked): // duty cycle of IO6 is 100%, duty cycle of IO5 is 100% forwardPin.setPulseWidth(period); reversePin.setPulseWidth(period); // Coast(no power output): // duty cycle of IO6 is 0%, duty cycle of IO5 is 0% forwardPin.setPulseWidth(period); reversePin.setPulseWidth(period);

BLE Controlling

Arduino 101 board has Bluetooth module and ZJS provides BLE API for it, so we can send directives from client (app or web page) to Arduino 101 by Bluetooth, RC car application receives and parses them to know which behaviors should be performed, then set the pulse width to respond correctly. Because BLE uses hexadecimal for data transfer, we design a data parsing protocol to improve readability:

Client sends an 8-bit string intending to turn right with 30 degrees and forward at 50% power, it is transmitted in hexadecimal format and received by car as 0x720x330x300x660x350x300x300x30. After that, parse the hexadecimal data to original string for setting each parameters. Example:

copy
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
var ble = require ("ble"); // Device name and id var DEVICE_NAME = 'ZJS Demo'; var gapUuid = 'fc00'; // Initial parameters var speed = 50; var angle = 30; var CPFlag = true; // Hex to Num var BuftoNum = function (buf) { var Num = 0; var checkValue = 0x30; for (var j = 0; j < 10; j++) { if (buf === checkValue + j) return Num + j; } } // Set car characteristic var DriverCharacteristic = new ble.Characteristic({ uuid: "fc0a", properties: ["read", "write"], descriptors: [ new ble.Descriptor({ uuid: "2901", value: "Driver" }) ] }); // Receive BLE request, and parse data to original DriverCharacteristic.onWriteRequest = function(data, offset, withoutResponse, callback) { // Filter invalid input if (data.length != 8) { // invalid protocol length callback(this.RESULT_INVALID_ATTRIBUTE_LENGTH); return; } else if ( data.readUInt8(0) != 0x66 && data.readUInt8(0) != 0x6c && data.readUInt8(0) != 0x72) { // invalid steering direction callback(this.RESULT_INVALID_ATTRIBUTE_LENGTH); return; } else if ( 0x34 < data.readUInt8(1) || data.readUInt8(1) < 0x30 || 0x39 < data.readUInt8(2) || data.readUInt8(2) < 0x30 || (data.readUInt8(1) === 0x34 && 0x35 < data.readUInt8(2))) { // invalid steering angle(> 45 degrees or < 0 degree) callback(this.RESULT_INVALID_ATTRIBUTE_LENGTH); return; } else if ( data.readUInt8(3) != 0x63 && data.readUInt8(3) != 0x62 && data.readUInt8(3) != 0x66 && data.readUInt8(3) != 0x72) { // invalid motor direction callback(this.RESULT_INVALID_ATTRIBUTE_LENGTH); return; } else if (0x39 < data.readUInt8(4) || data.readUInt8(4) < 0x30) || 0x39 < data.readUInt8(5) || data.readUInt8(5) < 0x30)) { // invalid motor power (> 99% or < 0%) callback(this.RESULT_INVALID_ATTRIBUTE_LENGTH); return; } // Parsing data following protocol // Steering angle var angleTens = BuftoNum(data.readUInt8(1)); var angleOnes = BuftoNum(data.readUInt8(2)); angle = angleTens * 10 + angleOnes; // Motor power var speedTens = BuftoNum(data.readUInt8(4)); var speedOnes = BuftoNum(data.readUInt8(5)); speed = speedTens * 10 + speedOnes; // Steering gear driving if (data.readUInt8(0) === 0x66) { // Turn Front } else if (data.readUInt8(0) === 0x6c) { // Turn Left with angle } else if (data.readUInt8(0) === 0x72) { // Turn Right with angle } // Motor driving if (data.readUInt8(3) === 0x63) { // Coast } else if (data.readUInt8(3) === 0x62) { // Brake } else if (data.readUInt8(3) === 0x66) { // Forward } else if (data.readUInt8(3) === 0x72) { // Reverse } console.log("receive basic data '" + data.toString('hex') + "'"); callback(this.RESULT_SUCCESS); }; ble.on("stateChange", function (state) { if (state === "poweredOn") { // BLE advertising // short url for physical web ble.startAdvertising(DEVICE_NAME, [gapUuid], "https://goo.gl/MVmzZW"); } else { if (state === 'unsupported') { // BLE not enabled on board } ble.stopAdvertising(); } }); // Advertising event ble.on("advertisingStart", function (error) { if (error) return; // BLE signal strength ble.updateRssi(); // Add car characteristic to BLE service ble.setServices([ new ble.PrimaryService({ uuid: gapUuid, characteristics: [ DriverCharacteristic ] }) ], function (error) { if (error) return; }); console.log("Advertising as Physical Web Service"); });

There is a RC car demo running on Arduino 101 and controlled by Android app named nRF Connect or Web app using Web Bluetooth.

  1. Get source code and build
copy
$ $ $ $ $ $ $
git clone http://github.com/haoxli/zephyr.js cd zephyr.js // switch to rc-car branch git checkout rc-car source zjs-env.sh source deps/zephyr/zephyr-env.sh make JS=demo/main.js
  1. Reset Arduino 101 to flash image
copy
$
make dfu
  1. Reset Arduino 101
  2. Open online page (https://haoxli.github.io/zephyr.js/blecontroller/) of BLE controller using Chrome browser (56+) on Android, connect to the device named ZJS Demo for controlling.

We can add other interesting features to RC car, such as auto-tracing, obstacle avoidance and joystick using provided ZJS APIs such as GPIO, ADC, I2C and TCP Sockets. In addition, ZJS supports OCF specification, which implements same JavaScript APIs based on iotivity-constrained. ZJS also has many other features to explore, pattern matching engine for example. ZJS will certainly help to develop awesome IoT applications.

Steps

1 total steps
1
DJ Injection