ハンコンラジコンの雑な解説
新年早々地震やら飛行機事故やらヤベー映像が飛び込んでくるけど、我が家は平常運転。今年もよろしく。
以前にも書いた気がするけど、なぜか俺のYouTubeチャンネルで再生回数最大なのは、会社の金で遊んだお勉強したハンコンラジコンの動画(現時点で再生回数8.2万回)。ある時インド人(?)が大挙して押し寄せてきて「くれくれ」コメントが付いたりもした(笑
そして最近になって、隣の部(というか当時の部の後継の部)のクルマ仲間の若者が同じようなことをやろうして俺のブログや動画に流れ着いたそうな(爆
そうかと思えば、同じようなタイミングでこの動画にまたコメントが付いて、なんか教えて欲しいっぽいので、ちょっと色々掘り起こしてみた。
まずは使った材料。
- ラジコン:タミヤのハイラックスハイリフト(プロポでシフトチェンジできるやつ)
- プロポ:フタバの4chのヤツATTACK 4YWD(サーボはそのまま利用。プロポ・受信機は使わない。ESCもMFC-02を利用。)
- マルチファンクションユニットMFC-02
- ラジコン側のラズパイ:Raspberry Pi Zero Wとユニバーサル基盤
- ステアリングコントローラー:プレステ3用のUSB接続のヤツ(Logicool Driving Force GT)
- ハンコン側のラズパイ:Raspberry Pi 2 (別にどれでもいいはず)
うむ、だいぶ忘れてる。そもそもラジコンに載せてたRaspberry Pi Zero Wのログインパスワード忘れてて難儀した。(´・ω・`)
ラジコン側のラズパイを接続する回路(電源&サーボやESCの信号線)
ちょっと回路図どっか行ってしまったけど、ラズパイZero用のユニバーサル基盤で、ラジコンの7.2Vからラズパイ用の5V電源を取り出しつつ(3端子レギュレータの型番忘れた…)、GPIOにラジコンの4ch分のサーボ・ESC信号線と接続するためのコネクタ(3pin×4)を配置。
ラジコン側プログラムのソースを見たところ、使ったGPIOは6, 13, 19, 26番(BCM番号)だったようだ。


ラジコンのバッテリーからの配線取り出しは、何かで見かけた分岐コネクタを利用。コネクタの種類が分からなくて、どこかの電子パーツ屋さんの通販サイトで見た目の近そうなヤツをいくつか買って見つけたんだけど、結局どれだったっけ?これ(EHコネクタ)だったかなあ?
うーん、タイトル通り雑な情報だ…。まあいいや、次。
ラジコン(サーバ)側のプログラム
- UDPで受信したパラメーターに応じてGPIOにPWM信号を出力する
- UDPの受信はこちらのページのソースをまねした
- GPIOのPWM信号出力は、pigpioのgpioServo()を利用
- アクセルペダルのアナログ値に応じてモーターを回転
- ステアリングのアナログ値に応じてステアリングのサーボを回転
- シフトUp/Downのボタンに応じてシフト段を加減算(-1,0,1,2,3)、シフト段に応じた位置にミッション用サーボを回転(ただし-1のときはバックとする。0はニュートラル、だったかな?)
- マルチファンクションユニットによるライトやウィンカー、ホーンなどのアクションも、それに応じたサーボ信号を疑似的に出力して再現。
// https://qiita.com/tajima_taso/items/fdfed88c1e735ffb41e8
#include <stdio.h> //printf(), fprintf(), perror(), getc()
#include <stdlib.h> //strtod()
#include <sys/socket.h> //socket(), bind(), sendto(), recvfrom()
#include <arpa/inet.h> // struct sockaddr_in, struct sockaddr, inet_ntoa(), inet_aton()
#include <stdlib.h> //atoi(), exit(), EXIT_FAILURE, EXIT_SUCCESS
#include <string.h> //memset(), strcmp()
#include <unistd.h> //close()
#include <time.h> //strftime(), localtime(), time()
#include <ctype.h> //isspace()
#include "udp.h"
#include <pigpio.h>
// サーボのピン番号(BCM番号)
#define PIN_SERVO_1_STEER 6
#define PIN_SERVO_2_ACCEL 13
#define PIN_SERVO_3_LIGHT 19
#define PIN_SERVO_4_SHIFT 26
// http://edu.clipper.co.jp/pg-2-39.html
void trim( char *s ) {
int i, j;
//文字列の最後から空白を読み飛ばして除外する
for( i = strlen(s)-1; i >= 0 && isspace( s[i] ); i-- ) ;
s[i+1] = '\0';
//先頭から空白でない文字まで読み飛ばす
for( i = 0; isspace( s[i] ); i++ ) ;
//前方の空白を詰める
if( i > 0 ) {
j = 0;
while( s[i] ) s[j++] = s[i++];
s[j] = '\0';
}
}
void shift_change(int shift) {
if (shift == 0)
gpioServo(PIN_SERVO_4_SHIFT, 1500 + 400);
else
gpioServo(PIN_SERVO_4_SHIFT, 1500 + 400 - 400*(shift-1));
}
int main(int argc, char* argv[]) {
const char *address = "";
unsigned short port = (unsigned short)atoi(argv[1]);
struct sockaddr servSockAddr, clitSockAddr;
char recvBuffer[MAX_BUFSIZE];
// UDPソケット初期化
int server_sock = get_socket("udp");
sockaddr_init(address, port, &servSockAddr);
if (bind(server_sock, &servSockAddr, sizeof(servSockAddr)) < 0) {
perror("bind() failed.");
exit(EXIT_FAILURE);
}
// pigpio初期化
if (gpioInitialise() < 0) {
printf("\nGPIO cannot initialised\n");
return 1;
}
gpioSetMode(PIN_SERVO_1_STEER, PI_OUTPUT);
gpioSetMode(PIN_SERVO_2_ACCEL, PI_OUTPUT);
gpioSetMode(PIN_SERVO_3_LIGHT, PI_OUTPUT);
gpioSetMode(PIN_SERVO_4_SHIFT, PI_OUTPUT);
gpioServo(PIN_SERVO_1_STEER, 1500);
gpioServo(PIN_SERVO_2_ACCEL, 1500);
gpioServo(PIN_SERVO_3_LIGHT, 1500);
gpioServo(PIN_SERVO_4_SHIFT, 1500 + 400);
// メインループ
int shift = 1; // シフト(1, 2, 3, 0(バック))
int brake = 0; // ブレーキ中=1
int back = 0; // バック中=1
while(1) {
char date[64];
int recvMsgSize = udp_receive(server_sock, recvBuffer, MAX_BUFSIZE, &clitSockAddr);
if (recvMsgSize == MSG_FAILURE) continue;
// バッファの末尾に\0追加
recvBuffer[recvMsgSize] = '\0';
// バッファの前後の空白・改行を除去(trimモドキ)
trim(recvBuffer);
// タイムスタンプを文字列化
// https://www.mm2d.net/main/prog/c/time-01.html
time_t t = time(NULL);
strftime(date, sizeof(date), "%Y/%m/%d %H:%M:%S", localtime(&t));
// デバッグ出力
printf("[%s] <%s> ", date,
inet_ntoa(((struct sockaddr_in *)&clitSockAddr)->sin_addr));
int sendMsgSize = udp_send(server_sock, "OK\n", &clitSockAddr);
if (sendMsgSize == MSG_FAILURE) continue;
// 受け取ったパラメータをパース
char key[MAX_BUFSIZE];
char val[MAX_BUFSIZE];
//parse_param(recvBuffer, key, val);
/*** keyの内容に応じて処理 ***/
// ステアリング
if (strncmp(recvBuffer, "STEERING=", 9) == 0) {
// 浮動小数点(-1.0~+1.0)を読み取る
double value = strtod(&recvBuffer[9], NULL);
printf("STEERING %f\n", value);
gpioServo(PIN_SERVO_1_STEER, 1500 - 400 * value);
}
// アクセル
else if (strncmp(recvBuffer, "ACCEL=", 6) == 0) {
// 浮動小数点(0.0~+1.0)を読み取る
double value = strtod(&recvBuffer[6], NULL);
// 0.1以下はスキップ
if (value < 0.1 && value != 0.0) {
printf("\n");
continue;
}
printf("ACCEL %f\n", value);
// バックの場合
if (shift == 0) {
// 符号を反転
value = -value;
// バックを離した瞬間
if (value == 0.0) back = 0;
// バックの踏み始めは一瞬バックを入れる
else if (back == 0) {
back = 1;
gpioServo(PIN_SERVO_2_ACCEL, 1500 + 100);
gpioSleep(PI_TIME_RELATIVE, 0, 200000);
gpioServo(PIN_SERVO_2_ACCEL, 1500);
gpioSleep(PI_TIME_RELATIVE, 0, 10000);
printf("BACK START!!\n");
}
}
gpioServo(PIN_SERVO_2_ACCEL, 1500 - 400 * value);
}
// ブレーキ
else if (strncmp(recvBuffer, "BRAKE=", 6) == 0) {
// 浮動小数点(0.0~+1.0)を読み取る
double value = strtod(&recvBuffer[6], NULL);
// 0.1以下はスキップ
if (value < 0.1 && value != 0.0) {
printf("\n");
continue;
}
printf("BRAKE %f\n", value);
// ブレーキを離した瞬間
if (value == 0.0) brake = 0;
// ブレーキの踏み始めは一瞬前進を入れる
else if (brake == 0) {
brake = 1;
gpioServo(PIN_SERVO_2_ACCEL, 1500 - 50);
gpioSleep(PI_TIME_RELATIVE, 0, 20000);
gpioServo(PIN_SERVO_2_ACCEL, 1500);
gpioSleep(PI_TIME_RELATIVE, 0, 10000);
printf("BRAKE START!!\n");
}
gpioServo(PIN_SERVO_2_ACCEL, 1500 + 400 * value);
}
// ホーン
else if (strcmp(recvBuffer, "HORN=ON") == 0) {
gpioServo(PIN_SERVO_3_LIGHT, 1500 - 400);
printf("HORN ON!!\n");
}
else if (strcmp(recvBuffer, "HORN=OFF") == 0) {
gpioServo(PIN_SERVO_3_LIGHT, 1500);
printf("HORN OFF!\n");
}
// ハイビーム(バンパー前のフォグ)
else if (strcmp(recvBuffer, "HIGHBEAM=ON") == 0) {
gpioServo(PIN_SERVO_3_LIGHT, 1500 - 300);
printf("HIGHBEAM ON!!\n");
}
else if (strcmp(recvBuffer, "HIGHBEAM=OFF") == 0) {
gpioServo(PIN_SERVO_3_LIGHT, 1500);
printf("HIGHBEAM OFF!\n");
}
// クラッチ
// シフト
else if (strcmp(recvBuffer, "SHIFT=UP") == 0) {
shift++;
if (shift > 3) shift = 3;
shift_change(shift);
printf("SHIFT UP!!i [%d]\n", shift);
}
else if (strcmp(recvBuffer, "SHIFT=DOWN") == 0) {
shift--;
if (shift < 0) shift = 0;
shift_change(shift);
printf("SHIFT DOWN!! [%d]\n", shift);
}
// ウィンカー(R2,L2)
else if (strcmp(recvBuffer, "WINKER=RIGHT") == 0) {
gpioServo(PIN_SERVO_1_STEER, 1500);
gpioServo(PIN_SERVO_3_LIGHT, 1500 + 400);
gpioSleep(PI_TIME_RELATIVE, 0, 100000); // sleep for 0.1 seconds
gpioServo(PIN_SERVO_3_LIGHT, 1500);
gpioSleep(PI_TIME_RELATIVE, 0, 100000); // sleep for 0.1 seconds
gpioServo(PIN_SERVO_1_STEER, 1500 - 120);
printf("WINKER RIGHT\n");
}
else if (strcmp(recvBuffer, "WINKER=LEFT") == 0) {
gpioServo(PIN_SERVO_1_STEER, 1500);
gpioServo(PIN_SERVO_3_LIGHT, 1500 + 400);
gpioSleep(PI_TIME_RELATIVE, 0, 100000); // sleep for 0.1 seconds
gpioServo(PIN_SERVO_3_LIGHT, 1500);
gpioSleep(PI_TIME_RELATIVE, 0, 100000); // sleep for 0.1 seconds
gpioServo(PIN_SERVO_1_STEER, 1500 + 150);
printf("WINKER LEFT\n");
}
else if (strcmp(recvBuffer, "WINKER=OFF") == 0) {
gpioServo(PIN_SERVO_1_STEER, 1500);
printf("WINKER OFF\n");
}
// ライト点灯(□)
else if (strcmp(recvBuffer, "LIGHT=UP") == 0) {
gpioServo(PIN_SERVO_1_STEER, 1500);
gpioServo(PIN_SERVO_3_LIGHT, 1500 + 50 + 400);
gpioSleep(PI_TIME_RELATIVE, 0, 100000); // sleep for 0.1 seconds
gpioServo(PIN_SERVO_3_LIGHT, 1500 + 50);
gpioSleep(PI_TIME_RELATIVE, 0, 100000); // sleep for 0.1 seconds
printf("LIGHT UP!\n");
}
// ハザード(△)
else if (strcmp(recvBuffer, "HAZARD=ON") == 0) {
gpioServo(PIN_SERVO_4_SHIFT, 1500 - 50);
gpioSleep(PI_TIME_RELATIVE, 0, 500000); // sleep for 0.1 seconds
gpioServo(PIN_SERVO_4_SHIFT, 1500 - 50 - 400);
gpioSleep(PI_TIME_RELATIVE, 0, 500000); // sleep for 0.1 seconds
gpioServo(PIN_SERVO_4_SHIFT, 1500 - 50);
gpioSleep(PI_TIME_RELATIVE, 0, 500000); // sleep for 0.1 seconds
// shift_change(shift);
printf("HAZARD ON!\n");
}
// エンジン停止・始動(PSボタン)
// 未対応のパラメータ
else {
printf("not supported. '%s'\n", recvBuffer);
}
}
// 終了処理
gpioTerminate();
return 0;
}
ハンコン(クライアント)側のプログラム
- ステアリングコントローラーからの値の読み取りはこのあたりを参考にした(linux/joystick.hを利用)
- アクセルペダル、ブレーキペダル、ステアリングなどの操作があったら、そのアナログ値を取得してUDPで送信
- シフトチェンジボタン、ウィンカー(R2, L2)、ホーン(GTボタン)、ライト点灯(□)、ハイビーム(○)、ハザード(△)、エンジン始動・停止(PSボタン)などのボタン入力があったら、それをUDPで送信
- UDPの送信はラジコン側と同様のページをマネっこ
// ステアリングコントローラーからの値の読み取りサンプル
// https://wlog.flatlib.jp/item/1682
// https://www.kernel.org/doc/Documentation/input/joystick-api.txt
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/joystick.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include "udp.h"
int main(int argc, char* argv[]) {
if (argc != 3) {
fprintf(stderr, "argument count mismatch error.\n");
exit(EXIT_FAILURE);
}
/*** UDP Socketの初期化(ここから) ***/
const char *address = argv[1];
unsigned short port = (unsigned short)atoi(argv[2]);
struct sockaddr servSockAddr, clitSockAddr;
struct sigaction action;
int server_sock = get_socket("udp");
sockaddr_init(address, port, &servSockAddr);
sigaction_init(&action, catchAlarm);
if (sigaction(SIGALRM, &action, NULL) < 0) {
perror("sigaction() failure");
exit(EXIT_FAILURE);
}
/*** UDP Socketの初期化(ここまで) ***/
// ステアリングコントローラーのデバイスファイルを開く
int fd = open("/dev/input/js0", O_RDONLY);
if (fd == -1) {
printf("Can't open /dev/input/js0\n");
exit(0);
}
int flag_hazard = 0; // 1(ON), 0(OFF)
int flag_shift = 0; // 3, 2, 1, 0(N), -1(R)
int flag_winker = 0; // 1(R), 0(OFF), -1(L)
// char flag_light = 0; // 1, 2, 3, 4
int flag_engine = 1; // 1(ON), 0(OFF)
while (1) {
struct js_event event;
char sendBuffer[MAX_BUFSIZE];
char receiveBuffer[MAX_BUFSIZE];
double value = 0.0;
// ステアリングコントローラーから読み取る
if (read(fd, &event, sizeof(event))
< sizeof(event)) {
continue;
}
switch (event.type & 0x7f) {
case JS_EVENT_BUTTON:
printf("BUTTON: number=%d, value=%d ",
event.number, event.value);
// ホーン(GTボタン)
if (event.number == 19) {
if (event.value == 1) {
udp_send(server_sock, "HORN=ON", &servSockAddr);
printf("HORN=ON");
}
else {
udp_send(server_sock, "HORN=OFF", &servSockAddr);
printf("HORN=OFF");
}
}
// ハイビーム(○ボタン)
else if (event.number == 2) {
if (event.value == 1) {
udp_send(server_sock, "HIGHBEAM=ON", &servSockAddr);
printf("HIGHBEAM=ON");
}
else {
udp_send(server_sock, "HIGHBEAM=OFF", &servSockAddr);
printf("HIGHBEAM=OFF");
}
}
// クラッチ
// シフトチェンジ(パドルまたはレバー)
else if ((event.number == 4 || event.number == 12 )
&& event.value == 1) {
udp_send(server_sock, "SHIFT=UP", &servSockAddr);
printf("SHIFT=UP");
}
else if ((event.number == 5 || event.number == 13 )
&& event.value == 1) {
udp_send(server_sock, "SHIFT=DOWN", &servSockAddr);
printf("SHIFT=DOWN");
}
// ウィンカー(R2,L2) ※改善の余地あり
// ウィンカー直後はステアリングをキャンセルしたい…
else if (event.number == 6 && event.value == 1) {
flag_hazard = 0;
udp_send(server_sock, "WINKER=RIGHT", &servSockAddr);
printf("WINKER=RIGHT");
}
else if (event.number == 7 && event.value == 1) {
flag_hazard = 0;
udp_send(server_sock, "WINKER=LEFT", &servSockAddr);
printf("WINKER=LEFT");
}
// ライト点灯(□)
else if (event.number == 1 && event.value == 1) {
udp_send(server_sock, "LIGHT=UP", &servSockAddr);
printf("LIGHT=UP");
}
// ハザード(△)
else if (event.number == 3 && event.value == 1) {
flag_hazard ^= 1;
if (flag_hazard) {
udp_send(server_sock, "HAZARD=ON", &servSockAddr);
printf("HAZARD=ON");
}
else {
udp_send(server_sock, "HAZARD=OFF", &servSockAddr);
printf("HAZARD=OFF");
}
}
// エンジン停止・始動(PSボタン)
else if (event.number == 20 && event.value == 1) {
flag_engine ^= 1;
if (flag_engine) {
udp_send(server_sock, "ENGINE=START", &servSockAddr);
printf("ENGINE=START");
}
else {
udp_send(server_sock, "ENGINE=STOP", &servSockAddr);
printf("ENGINE=STOP");
}
}
printf("\n");
break;
case JS_EVENT_AXIS:
printf("AXIS: number=%d, value=%d ",
event.number, event.value);
// ステアリング
if (event.number == 0) {
value = event.value / 32767.0;
sprintf(sendBuffer, "STEERING=%f", value);
udp_send(server_sock, sendBuffer, &servSockAddr);
printf(sendBuffer);
}
// アクセル(とりあえず前進だけ)
else if (event.number == 1) {
value = (32767.0 - event.value) / (32767*2);
sprintf(sendBuffer, "ACCEL=%f", value);
udp_send(server_sock, sendBuffer, &servSockAddr);
printf(sendBuffer);
}
// ブレーキ
else if (event.number == 2) {
value = (32767.0 - event.value) / (32767*2);
sprintf(sendBuffer, "BRAKE=%f", value);
udp_send(server_sock, sendBuffer, &servSockAddr);
printf(sendBuffer);
}
printf("\n");
break;
default:
printf("OTHER\n");
break;
}
}
// 終了処理
close(fd);
return 0;
}
プログラム一式
https://www.tapoblog.0t0.jp/wp-content/uploads/SocketRC_20181031.tar.gz
一応Makefileやreadme.txtも入れてあるので、コンパイルや実行はできると思う。普通の2chラジコンでも、アクセル・ブレーキ・ステアリングは動くと思う。
ダサいプログラムでちょっと恥ずかしいけど、こんな感じで参考になったかなあ?