Vi så sidste gang at når først vores Wii Balance Board var parret, dukkede det automatisk op i device-træet som en HID input enhed som kunne findes via DEVPATH environment variablen sat af udev. Nedenfor er en anden måde at se hvilke input devices der er
$ cat /proc/bus/input/devices .... I: Bus=0005 Vendor=057e Product=0306 Version=0600 N: Name="Nintendo Wii Remote Balance Board" P: Phys= S: Sysfs=/devices/platform/soc/3f201000.serial/tty/ttyAMA0/hci0/hci0:11/0005:057E:0306.0002/input/input1 U: Uniq= H: Handlers=event1 js0 B: PROP=0 B: EV=b B: KEY=10000 0 0 0 0 0 0 0 0 0 B: ABS=f0000
enheden er tilgængelig både som input1 og js0 der er den gamle devicetype for joysticks. Al ny kode bør bruge input.
Flowet i en vejning er:
- udev kalder via /etc/udev/rules.d/90-wiibb.rules wiibb.sh
- wiibb.sh er et shell-script der klarer det overordnede program-flow, start af programmer, debug logs, sende resultatet videre, lukke bluetooth ned…
- /usr/local/bin/wiibb skrevet i C wiibb.c laver selve vejningen.
funktionalitet skal være:
- Ingen parrings funktionalitet i wiibb.sh eller wiibb.c
- tryk på knappen på Wii-Fit for at tænde den
- blink LED et par gange – sker automatisk
- forbind program med enheden – udev kalder wiibb.sh
- tænd LED – indikerer at vi måler – wiibb.sh
- aflæs sensorer og middel den indtil den er stabil – wiibb.c
- sluk LED – sker automatisk ved bluetooth disconnect
- luk enheden ned – disconnect wii-fit wiibb.sh
- gem/udskriv målingen og tidspunktet –wiibb.sh
Det første man skal gøre er at læse manual-siden, desværre er der ingen for input. Det næste er at læse include filer, og da det er en driver, er det også værd at kigge i kildeteksten til linux-kernen, og så er der google. Her er hvad jeg skimmede igennem:
- The Linux USB Input Subsystem, Part I, Linux Journal 2003
- The Linux USB Input Subsystem, Part II, Linux Journal 2003
- Documentation/input/input.txt
- Documentation/input/input-programming.txt
- Documentation/ABI/testing/sysfs-driver-hid-wiimote
- include/linux/input.h
- drivers/hid/hid-wiimote.h
- drivers/hid/hid-wiimote-modules.c
kildeteksten til selve driveren behøver man sikkert ikke at konsultere, men det godt at vide at der er fuld dokumentation.
Vores Wii Balance Board er tilgængelig via /dev/input/event1, Events får man fat i via read() ind i en structur der indeholder time-stamp, type, code, value, for at se på hvad det er vi har med at gøre, har jeg skrevet dette lille C-program
/* * read_events.c - Copyright (c) 2017 peter@lorenzen.us * This program is free software, BSD-licensed */ #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <sys/ioctl.h> #include <sys/time.h> #include <linux/input.h> void usage(char *mess) { printf("read_events path\n"); printf("f.ex path=/dev/input/event1\n"); printf("%s\n",mess); exit(1); } int main(int argc, char **argv) { int fd; if ((fd = open(argv[1], O_RDONLY)) < 0) { usage("cannot open device"); } char name[80] = "Got nothing expected Nintendo"; int r = ioctl(fd, EVIOCGNAME(sizeof(name)), name); printf("Events for \"%s\" on device %s\n", name, argv[1]); int first_sec=0; struct input_event ev; while (read(fd, &ev, sizeof(ev)) == sizeof(ev)) { if (first_sec==0) first_sec = ev.time.tv_sec; int sec = ev.time.tv_sec - first_sec; int usec = ev.time.tv_usec; // to avoid warning printf("sec %d.%06d type=0x%04x code=0x%04x value=0x%08x\n", sec, usec, ev.type, ev.code, ev.value); } perror("error reading event"); }
Output af read_events dev/input/event1 ser således ud:
Events for "Nintendo Wii Remote Balance Board" on device /dev/input/event1 sec 0.803470 type=0x0003 code=0x0012 value=0x00000018 sec 0.803470 type=0x0000 code=0x0000 value=0x00000000 sec 0.805909 type=0x0003 code=0x0011 value=0x00000013 sec 0.805909 type=0x0003 code=0x0012 value=0x00000011 sec 0.805909 type=0x0003 code=0x0013 value=0x00000041 sec 0.805909 type=0x0000 code=0x0000 value=0x00000000 sec 0.823412 type=0x0003 code=0x0011 value=0x00000014 sec 0.823412 type=0x0003 code=0x0012 value=0x0000000f sec 0.823412 type=0x0003 code=0x0013 value=0x00000048 sec 0.823412 type=0x0000 code=0x0000 value=0x00000000 ...
Fra /usr/include/linux/input.h finder vi disse definitioner
/* excerpts from <linux/input.h> */ struct input_event { struct timeval time; __u16 type; __u16 code; __s32 value; }; /* Event types */ #define EV_SYN 0x00 #define EV_KEY 0x01 #define EV_ABS 0x03 /* Absolute axes */ #define ABS_HAT0X 0x11 #define ABS_HAT0Y 0x11 #define ABS_HAT1X 0x12 #define ABS_HAT1Y 0x13
Dermed kan vi vi skrive en første version af vores program wiibb.c
/* * wiibb.c - Copyright (c) 2017 peter@lorenzen.us * This program is free software - BSD-licensed */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <unistd.h> #include <time.h> #include <sys/ioctl.h> #include <sys/time.h> #include <linux/input.h> void usage(char *mes) { printf("Usage: wiibb event_device\n"); printf(" print sensor values and total weight\n"); if (mes) printf("%s\n",mes); exit(1); } time_t start_sec; typedef struct { int usec; // 12:10:10 seconds:msec:usec int button; int sensor[4]; // br tr bl tl bottom/top right/left int total; } sample_t; void handle_sample(sample_t *s) { static int prev_usec = 0; printf("%d.%03d %d %d %d %d %d %d\n", s->usec>>20, (s->usec & 0xfffff)>>10, (s->usec - prev_usec)>>10, s->total, s->sensor[0], s->sensor[1], s->sensor[2], s->sensor[3]); prev_usec = s->usec; } int main(int argc, char **argv) { if (argc != 2) { usage(NULL); } int fd; if ((fd = open( argv[argc-1], O_RDONLY)) < 0) { usage("cannot open device"); } char name[80] = "Got nothing expected Nintendo"; int r = ioctl(fd, EVIOCGNAME(sizeof(name)), name); printf("# %s \"%s\" margin=%d, divisor=%d option_d=%d\n", getenv("DEVNAME"), name, margin, divisor, option_d); if (r<0) { usage("ioctl name failed"); } printf("# t.ms + 10g br tr bl tl\n"); sample_t s = { .usec=-1, .button=0, .sensor={0,0,0,0}}; struct input_event ev; while (read(fd, &ev, sizeof(ev)) == sizeof(ev)) { if (start_sec==0) { start_sec = ev.time.tv_sec; } else if (ev.time.tv_sec > (60+start_sec)) { printf("timeout after 60 seconds\n"); exit(0); } if (ev.type == EV_SYN) { // end if this time_stamp, s.usec = (ev.time.tv_sec - start_sec) * 1024*1024; // >> 20 will give seconds s.usec += ev.time.tv_usec; s.total = s.sensor[0]+s.sensor[1]+s.sensor[2]+s.sensor[3]; handle_sample( &s); } else if (ev.type == EV_KEY) { if (ev.value) { printf("button pressed\n "); exit(0); } } else if (ev.type == EV_ABS) { s.sensor[0x3 & ev.code] = ev.value; } else { printf("UNKNOWN type=0x%x code=0x%x value=0x%x\n", ev.type, ev.code, ev.value); } } perror("wiibb: error reading"); }
Nu ser output fra programmet således ud
# /dev/input/event16 "Nintendo Wii Remote Balance Board" margin=10, divisor=100 option_d=1 # t.ms + 10g br tr bl tl 0.876 2 256 0 72 56 128 0.903 26 280 33 73 54 120 0.906 3 298 37 80 53 128 0.925 18 297 37 80 52 128 0.926 1 284 33 85 44 122 0.943 17 296 34 84 42 136 0.960 17 284 28 95 33 128 0.966 6 273 29 85 32 127 1.017 74 274 30 92 26 126
Det ser ud som om der er støj på dataene, så lad os kigge nærmere på dem via gnuplot, vi tager en 5 sekunders måling og plotter resultat. For at komme fri af nul-punktet vejer vi 4 styk 500gram Blå cirkel kaffe som jeg har bragt hjem fra Danmark til dette formål, scriptet ser således ud:
#!/bin/sh # wiibb -a > fil; wiibb_gnuplot.sh < fil # t.ms + 10g br tr bl tl sec=1;kg=3;br=4;tr=5;bl=6;tl=7 FILE=$(basename $1 .log)-sensor # scale total by 1/4 to have equal scales awk 'NF==7 { $3=$3/4;print $0}' < $1 > $FILE.dat f=`head -1 $FILE.dat | cut -f1 -d:` t=`tail -1 $FILE.dat | cut -f1 -d:` echo gnuplot of $FILE.dat over $t seconds to $FILE.png gnuplot << EOF set term png size 1024,512 set output "$FILE.png" set autoscale set xlabel "seconds" set ylabel "sensor" set y2label "kg" set y2tics set link y2 via y/25 inverse y*25 plot ["$f":"$t"] \ '$FILE.dat' using 1:$kg title "kg" with lines lw 3, \ '$FILE.dat' using 1:$br title "bottom right" with lines, \ '$FILE.dat' using 1:$tr title "top right" with lines, \ '$FILE.dat' using 1:$bl title "bottom left" with lines, \ '$FILE.dat' using 1:$tl title "top left" with lines EOF
Da jeg prøvede nogle af de eksisterende programmer i mit første indlæg, var der meget flicker på de sidste cifre, og som man ser er der periodisk støj af forskellig størrelse på de forskellige sensorer, første indskydelse er at det hidrører fra netstøj og er på en eller anden måde relateret til 60 Hz.
Et simpel lavpas filtrering kan filtrere støjen væk der er 50-60 samples pr sekund. Inden vi laver vores endelige C-program laver vi lige et par test offline i AWK.
Lavpas filteret bidrager med 1% af forskellen til resultatet hver gang, da der er 50-60 samples pr sekund vil det tage et nogle sekunder før resultatet vil stabilisere sig, derfor bruger vi en meget større faktor 20% når forskellen er mere end 5 kg, idet vi antager at støjen er markant mindre end det.
Når resultatet ikke ændrer sig mere end +- 0.1 kg inden for 1 sekund, betragter vi resultatet som stabilt og målingen afsluttes.
Parametrene kan justeres ved at sætte env-variablerne FACTOR=0.01 og MARGIN=10 (0.1kg),
#!/bin/sh # wiibb /dev/input/event1 > fil; plot_filter.sh < fil # t.ms + xgram br tr bl tl sec=1;xgram=3;br=4;tr=5;bl=6;tl=7 FILE=$(basename $1 .log)-filter awk 'BEGIN { margin='${MARGIN:-0.1}' lowpass = 0 last_lp = 0 printf("# t raw lowpass diff sec_settled\n");} NF==7 { kg = $3/100.0 diff = lowpass - kg if ((diff > 5) || (diff < -5)) { # more than 5 kg diff this is not noise factor = 0.2 # adjust fast } else { factor = '${FACTOR:-0.01}' } lowpass -= diff * factor if ((last_lp < (lowpass-margin)) || (last_lp > (lowpass+margin)) || (kg < 5)) { last_lp = lowpass; last_time = $1 } else if (($1-last_time) > 1.0) { printf("# at t = %1.3f settled at %1.3f kg +- %1.3f for %s seconds\n", $1, lowpass, margin, $1-last_time) printf("at t = %1.3f settled at %1.3f kg +- %1.3f for %s seconds\n", $1, lowpass, margin, $1-last_time) > "/dev/tty" } printf("%1.3f %1.2f %1.2f %1.2f %d # last=%1.3f\n", $1, kg, lowpass, diff, 100*($1-last_time), last_time) }' <$1 >$FILE.dat if [ $# = 2 ]; then from=$1 to=$2 else from=`awk '! /^#/ {print $1;exit}' $FILE.dat` to=`awk 'END {print $1;exit}' $FILE.dat` fi echo gnuplot of $FILE.dat $from - $to seconds to $FILE.png gnuplot << EOF set term png size 1024,512 set output "$FILE.png" set autoscale set xlabel "seconds" set ylabel "kg" set ytics nomirror set y2label "settled in msec." set y2tics set link y2 via y*10 inverse y/10 plot ["$from":"$to"] \ '$FILE.dat' using 1:2 title "raw" with lines, \ '$FILE.dat' using 1:3 title "lowpass factor ${FACTOR:-0.01}" with lines, \ '$FILE.dat' using 1:4 title "diff between two kg samples" with lines, \ '$FILE.dat' using 1:5 title "ms settled within ${MARGIN:-0.1} kg" with lines EOF
Plottet af min vejning ser således ud
Det endelige C-program
Så er der bare tilbage at kopiere funktionaliteten fra AWK-script over i C-programmet, det mest funktionen handle_sample() der skal rettes til, resultatet er:
/* * wiibb.c - Copyright (c) 2017 peter@lorenzen.us * This program is free software - BSD-licensed */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <unistd.h> #include <time.h> #include <sys/ioctl.h> #include <sys/time.h> #include <linux/input.h> void usage(char *mes) { printf("Usage: wiibb [-d] event_device\n"); printf(" filter and determine the weight on the balance board\n"); if (mes) printf("%s\n",mes); exit(1); } int option_d=0; #define dprint(...) option_d && fprintf(stderr, __VA_ARGS__) #define ddprint(...) (option_d>1) && fprintf(stderr, __VA_ARGS__) int margin = 10; // 100 gram int divisor = 100; time_t start_sec; typedef struct { int usec; // 12:10:10 seconds:msec:usec int button; int sensor[4]; // br tr bl tl bottom/top right/left int total; } sample_t; void handle_sample(sample_t *s) { static int prev_usec = 0; static int xgram = 0; // each unit is ten gram static int last_xgram = 0; static int last_usec = 0; int diff = xgram - s->total; if ((diff > 500) || (diff < -500)) { // more than 5 kg diff this is not noise xgram -= diff / 5; // adjust fast } else { xgram -= diff / divisor; } dprint("%d.%03d %d %d %d %d %d %d", s->usec>>20, (s->usec & 0xfffff)>>10, (s->usec - prev_usec)>>10, s->total, s->sensor[0], s->sensor[1], s->sensor[2], s->sensor[3]); ddprint("# last_usec: %d.%03d xgram: %d last_xgram: %d\n", last_usec>>20, (last_usec & 0xfffff)>>10, xgram, last_xgram ); dprint("\n"); prev_usec = s->usec; if ((last_xgram < (xgram-margin)) || (last_xgram > (xgram+margin)) || (xgram < 500)) { last_xgram = xgram; // start a new stable last_usec = s->usec; } else if ((s->usec-last_usec) > (1024*1024)) { // stable for a second, we are DONE char date_time[100]; time_t sec = start_sec + s->usec/(1024*1024); strftime(date_time, sizeof(date_time), "%Y%m%d-%H%M", localtime(&sec)); printf("#date: %s kg: %1.2f lbs: %1.1f\n", date_time, xgram/100.0, xgram/45.4); exit(0); } } int main(int argc, char **argv) { if (argc==3) { if (strcmp(argv[1], "-d")==0) { option_d = 1; } else if (strcmp(argv[1], "-dd")==0) { option_d = 2; } } else if (argc != 2) { usage(NULL); } char *env; if ((env = getenv("MARGIN"))) { margin = atoi(env); // 100 gram } if ((env = getenv("DIVISOR"))) { divisor = atoi(env); } int fd; if ((fd = open( argv[argc-1], O_RDONLY)) < 0) { usage("cannot open device"); } char name[80] = "Got nothing expected Nintendo"; int r = ioctl(fd, EVIOCGNAME(sizeof(name)), name); dprint("# %s \"%s\" margin=%d, divisor=%d option_d=%d\n", getenv("DEVNAME"), name, margin, divisor, option_d); if (r<0) { usage("ioctl name failed"); } dprint("# t.ms + 10g br tr bl tl\n"); sample_t s = { .usec=-1, .button=0, .sensor={0,0,0,0}}; struct input_event ev; while (read(fd, &ev, sizeof(ev)) == sizeof(ev)) { if (start_sec==0) { start_sec = ev.time.tv_sec; } else if (ev.time.tv_sec > (60+start_sec)) { printf("timeout after 60 seconds\n"); exit(0); } if (ev.type == EV_SYN) { // end if this time_stamp, s.usec = (ev.time.tv_sec - start_sec) * 1024*1024; // >> 20 will give seconds s.usec += ev.time.tv_usec; s.total = s.sensor[0]+s.sensor[1]+s.sensor[2]+s.sensor[3]; handle_sample( &s); } else if (ev.type == EV_KEY) { if (ev.value) { printf("button pressed\n "); exit(0); } } else if (ev.type == EV_ABS) { s.sensor[0x3 & ev.code] = ev.value; } else { printf("UNKNOWN type=0x%x code=0x%x value=0x%x\n", ev.type, ev.code, ev.value); } } perror("wiibb: error reading"); }
Det endelige wrapper script wiibb.sh
I begyndelse køres scriptet med debug print på så der bliver lidt statistik at kigge på senere, og måske optimere MARGIN og DIVISOR eller hvad der nu måtte vise sig. Filerne ligger under /var/log/wiibb
!/bin/bash # called via this udev rule: # SUBSYSTEM=="input", DEVPATH=="*:057E:0306*", ACTION=="add" RUN+="/bin/sh -c '/usr/local/sbin/wiibb.sh >>/tmp/wiibb.log 2>&1'" # we rely on DEVPATH and DEVNAME, the rest we get from sysfs #export PATH=/usr/local/bin:/usr/local/sbin:$PATH set -x DEVICE=/sys/$DEVPATH/device LED=$DEVICE/leds/*:057E:0306*:blue:p0/brightness # sets HID_UNIQ . $DEVICE/uevent MAC=$(echo $HID_UNIQ | tr a-f A-F) DATE_TIME=$(date "+%Y%m%d-%H%M") set echo Wii Board $MAC accessed via $DEVNAME $DEVPATH | sudo tee /dev/kmsg >/dev/console echo 1 > $LED; sleep 1; # when it turns off it indicates device is off /usr/local/sbin/wiibb -dd $DEVNAME >/tmp/wiibb-$DATE_TIME.log1 2>/tmp/wiibb-$DATE_TIME.log2 if dpkg --compare-versions $(bluetoothctl -v) lt 5.43 ; then hcitool dc $MAC # use hcitool to disconnect if bluetoothctl is old else echo disconnect $MAC | bluetoothctl fi KG=$(cut -f4 -d' ' /tmp/wiibb-$DATE_TIME.log1) mkdir -p /var/log/wiibb cat /tmp/wiibb-$DATE_TIME.log1 /tmp/wiibb-$DATE_TIME.log2 | xz -z >/var/log/wiibb/$DATE_TIME-$KG.xz rm -f /tmp/wibb-$DATE_TIME* echo "WiiBB $MAC $_DATE_TIME $KG kg"| sudo tee /dev/kmsg >/dev/console
/etc/udev/rules.d/90-wiibb.rules
# Udev wiibb Call wiibb.sh when ever Wii Balanaceboard is turned on SUBSYSTEM=="input", ENV{DEVNAME}=="/dev/input/event*", DEVPATH=="*:057E:0306*", ACTION=="add", RUN+="/bin/sh -c '/usr/local/sbin/wiibb.sh $DEVNAME >>/tmp/wiibb.log 2>&1'"
Nødvendige programmer der skal installeres
Listen her bliver sikkert mere udførlig engang jeg skal installere det på en ny maskine, men der skal ihverfald installeres disse pakker
sudo apt-get install xz gnuplot # raspbian pre Stretch (før sommer 2017) installer gnuplot5 sudo apt-get install libxwiimote-dev xwiimote libxwiimote2
You must be logged in to post a comment.