Badevægt – wiibb.c – dobbelt-v dobbelt-i dobbelt-b

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:

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

og plottet bliver:

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

This entry was posted in Bluetooth, Linux. Bookmark the permalink.