Fun with PIV with my YubiKey 3 Neo

Turns out that my マイナンバーカード is not the only thing which can do things like signing files and PIV is the official(?) standard for this. My old Yubikey 3 Neo can do that too thanks to the yubico-piv-tool.

And it’s basically the same as the マイナンバーカード except I have to set up everything myself.

As on my マイナンバーカード there’s 2 slots for 2 different keys and certificates:

  • Slot 9a for identification
  • Slot 9c for signing

Creating them is simple. I just show the ones for the signing slot 9c:

❯ yubico-piv-tool -s9c -AECCP256 -agenerate -o f2-9c.pub
❯ yubico-piv-tool -s9c -S'/CN=Harald Kubota/OU=Home/O=lan/' -averify -arequest -i f2-9c.pub -o f2-9c.csr
Enter PIN: 
Successfully verified PIN.
Successfully generated a certificate request.
# I need a DNS Name. And 8670h is about 1 year.
❯ step ca sign --set=dnsNames='["test5.lan"]' --not-after=8760h f2-9c.csr f2-9c.crt
✔ Provisioner: myCA@home (JWK) [kid: IFXxmmZDCX76WMNbFfUoBOBZdubx0SG45Jsd0VGxaz1]
✔ Please enter the password to decrypt the provisioner key: 
✔ CA: https://ca.lan:8443
✔ Certificate: f2-9c.crt
❯ yubico-piv-tool -s9c -aimport-certificate -i f2-9c.crt
Successfully imported a new certificate.

And here is how to sign a file and verify the signature:

❯ yubico-piv-tool -averify-pin --sign -s9c -HSHA256 -AECCP256 -i test.txt -o test.signature
Enter PIN: 
Successfully verified PIN.
Signature successful!

❯ yubico-piv-tool -s9c -aread-certificate >f2-9c.crt
❯ openssl x509 -pubkey -in f2-9c.crt -noout > f2-9c.pub
❯ openssl dgst -sha256 -verify f2-9c.pub -signature test.signature test.txt
Verified OK

マイナンバーカード fun!

Got a Smart Card reader, so the fun can begin!

PDF Signing

This is most common, so it’s best documented.

For Windows you need:

Old non-Unicode software, not well tested on non-Japanese PCs
  • Optional: Import the root certificate as it’s documented in the English description.
  • PDF Signing software from here
  • Usage is simple enough. Worked on my first try.
  • To test, get Acrobat Reader DC. Don’t forget to un-click all unwanted extra-software.
  • In Acrobat, go to Edit/Preferences and click on Signatures/Verification’s More… button and enable Windows Integration:
  • Alternatively import the user signing CA certificate into Acrobat Reader so it trusts it.

Now the previously signed PDF should show up in Acrobat as signed:

Non-PDFs

Far more interesting (for me) is signing arbitrary files to proof that those are “mine”.

myna is a simple program for Windows, Linux and Mac to do all basic things the マイナンバーカード can do. Usage is simple (Windows here):

> myna.exe jpki cms sign -i INPUTFILE -o OUTPUTFILE
> myna.exe jpki cms verify FILE

Note that the both command needs the マイナンバーカード available. The OUTPUTFILE is in PKCS#7 format. Verification via openssl is possible:

$ openssl cms -verify -inform DER -in FILE -CAfile jpki.pem

The jpki.pem is the PEM encoded root certificate from your マイナンバーカード . But in return you don’t need the card at this point.

If you want to see who signed:

$ openssl pkcs7 -print_certs -inform DER -in test-signed.p7m -noout
subject=C = JP, L = Tokyo-to, L = Xxx-shi, CN = 
2xxxxxxxxxxxxxxxxxxxxxxxxxxA

issuer=C = JP, O = JPKI, OU = JPKI for digital signature, OU = Japan Agency for Local Authority Information Systems

# Many more cert details:
$ openssl pkcs7 -print_certs -inform DER -in test-signed.p7m -noout -text

# Actual verify:
$ openssl cms -verify -inform DER -in test-signed.p7m -CAfile jpki.pem -cmsout -print
CMS_ContentInfo:
  contentType: pkcs7-signedData (1.2.840.113549.1.7.2)
  d.signedData:
    version: 1
    digestAlgorithms:
        algorithm: sha1 (1.3.14.3.2.26)
        parameter: <ABSENT>
    encapContentInfo:
      eContentType: pkcs7-data (1.2.840.113549.1.7.1)
      eContent:
        0000 - 4d 6f 64 65 6c 73 50 61-74 68 3d 22 4d 6f 64   ModelsPath="Mod
[...]
        00b4 - 76 65 32 44 22 0a                              ve2D".
    certificates:
      d.certificate:
        cert_info:
          version: 2
          serialNumber: 4xxxxxx5
          signature:
            algorithm: sha256WithRSAEncryption (1.2.840.113549.1.1.11)
            parameter: NULL
          issuer: C=JP, O=JPKI, OU=JPKI for digital signature, OU=Japan Agency for Local Authority Information Systems
          validity:
            notBefore: Feb 15 19:11:22 2021 GMT
            notAfter: Xxx xx 14:59:59 2025 GMT
          subject: C=JP, L=Tokyo-to, L=Xxx-shi, CN=2xxxxxxxxxxxxxxxxxxxxxxxxxxA
          key:
[...]

You should be able to recognize the signing serial number: that’s the user certificate on the マイナンバーカード.

Other Notes

There’s 4 certificates on the card:

  • user certificate
  • the CA’s public certificate which signed the user certificate
  • user signing certificate
  • the CA’s public certificate which signed the user signing certificate

asn1decode shows the internal structure of data, but a lot of data is hidden in “OCTET STRING” like this:

 1637:d=8  hl=3 l= 176 cons: SEQUENCE
 1640:d=9  hl=2 l=   3 prim: OBJECT            :X509v3 Authority Key Identifier
 1645:d=9  hl=3 l= 168 prim: OCTET STRING      [HEX DUMP]:3081A580144DE017DE4B7F473DCD867A62D38B134ACE83558AA18186A48183308180310B3009060355040613024A50310D300B060355040A0C044A504B4931233021060355040B0C1A4A504B4920666F72206469676974616C207369676E6174757265313D303B060355040B0C344A6170616E204167656E637920666F72204C6F63616C20417574686F7269747920496E666F726D6174696F6E2053797374656D7382040132C4AB
 1816:d=8  hl=2 l=  29 cons: SEQUENCE
 1818:d=9  hl=2 l=   3 prim: OBJECT            :X509v3 Subject Key Identifier

You can display that data in the binary blob like this:

harald@r2s1:~/t$ openssl asn1parse -inform DER -in test-signed.p7m -strparse 1645
    0:d=0  hl=3 l= 165 cons: SEQUENCE
    3:d=1  hl=2 l=  20 prim: cont [ 0 ]
   25:d=1  hl=3 l= 134 cons: cont [ 1 ]
   28:d=2  hl=3 l= 131 cons: cont [ 4 ]
   31:d=3  hl=3 l= 128 cons: SEQUENCE
   34:d=4  hl=2 l=  11 cons: SET
   36:d=5  hl=2 l=   9 cons: SEQUENCE
   38:d=6  hl=2 l=   3 prim: OBJECT            :countryName
   43:d=6  hl=2 l=   2 prim: PRINTABLESTRING   :JP
   47:d=4  hl=2 l=  13 cons: SET
   49:d=5  hl=2 l=  11 cons: SEQUENCE
   51:d=6  hl=2 l=   3 prim: OBJECT            :organizationName
   56:d=6  hl=2 l=   4 prim: UTF8STRING        :JPKI
   62:d=4  hl=2 l=  35 cons: SET
   64:d=5  hl=2 l=  33 cons: SEQUENCE
   66:d=6  hl=2 l=   3 prim: OBJECT            :organizationalUnitName
   71:d=6  hl=2 l=  26 prim: UTF8STRING        :JPKI for digital signature
   99:d=4  hl=2 l=  61 cons: SET
  101:d=5  hl=2 l=  59 cons: SEQUENCE
  103:d=6  hl=2 l=   3 prim: OBJECT            :organizationalUnitName
  108:d=6  hl=2 l=  52 prim: UTF8STRING        :Japan Agency for Local Authority Information Systems
  162:d=1  hl=2 l=   4 prim: cont [ 2 ]

Got my マイナンバーカード!

For anyone outside Japan this is probably not of any interest. Please pass. Nothing to see here.

For me it was interesting: this is a smart card which can also use NFC, which makes it very interesting: How does it work? What data is inside? Can I look at it? Can other people look at it (without the PIN)? Why does it have 2 different PINs?

Things I learned in half a day since I got my MyNumber card:

  • It can do NFC too.
  • This app works to read data off the card. Including pictures, certificates and other stuff. Interesting.
  • On this site it explains the file system structure and other internals of the card. Very interesting.
  • That unrelated app works great to read my Suica/PASMO card. And both apps work and they figure out which card is for which app. Neat.
  • Here is a 5 year old article about how to use the data via its PKCS#11 API. And how to use this for ssh with OpenSC. I did that a while ago with a YubiKey. I prefer the YubiKey form factor a lot.
  • I got so many gadgets at home, but no Smart Card reader/writer. I should get this fixed so I can read the certificate with my PC. Makea paying tax via e-Tax much easier too.

Google Cloud Platform

My AWS Certified Solution Architect – Professional is expiring in June! Since renewing it is a bit boring, it’s a great reason to get to know GCP better. I generally like their way of thinking more and today I understood why:

  • AWS has DevOps as their focus point for many products
  • GCP has the developer as the focus point for many products

Of course there’s plenty overlap, but the philosophy is fundamentally different. But that might just be my opinion. It would explain why I am more comfortable with AWS with my Sysadmin background, but more curious with GCP (as a wanna-be small-scale developer).

Pub/Sub

Beside creating VMs, traditionally one of the easiest ways to interact with a cloud environment is message queues. In GCP this is Pub/Sub. And it’s easy.

  1. Create a Topic. With a schema (to keep yourself sane).

Schema (AVRO):

{
  "type": "record",
  "name": "Avro",
  "fields": [
    {
      "name": "Sensor",
      "type": "string"
    },
    {
      "name": "Temp",
      "type": "int"
    }
  ]
}

Then you can publish via gcloud (thanks to Pavan for providing a working example):

❯ gcloud pubsub topics publish Temp --message='{"Sensor":"Storage","Temp":9}'

And in Node.js:

const {PubSub} = require('@google-cloud/pubsub');

function main(
  topicName = 'Temp',
  data = JSON.stringify({Sensor: 'Living room', Temp: 22})
) {

  const pubSubClient = new PubSub();

  async function publishMessage() {
    const dataBuffer = Buffer.from(data);

    try {
      const messageId = await pubSubClient.topic(topicName).publish(dataBuffer);
      console.log(`Message ${messageId} published.`);
    } catch (error) {
      console.error(`Received error while publishing: ${error.message}`);
      process.exitCode = 1;
    }
  }

  publishMessage();
}

process.on('unhandledRejection', err => {
  console.error(err.message);
  process.exitCode = 1;
});

main(...process.argv.slice(2));

And with plumber:

# Subscribe
❯ plumber read gcp-pubsub --project-id=training-307604 --sub-id=Temp2-sub -f

# Publish
❯ plumber write gcp-pubsub --topic-id=Temp --project-id=training-376841 --input-data='{"Sensor":"Kitchen","Temp":19}'

M5Stack & AWS IoT

Received my AWS IoT EduKit from M5Stack. First impression: it’s an improvement to the original M5Stack I have: display is nicer and power and reset button is now 2 instead of 1 button. Sensor touch instead of buttons too, but not sure this is an improvement or just cost cutting.

Time to test this via the Blinky Hello World example!

First notes: the instructions do clash with any Python environment you might have set up. It recommends to use miniconda. It’s not needed if you already have a virtual environment setup. Just activate your environment:

$ cd ~/git
$ git clone -b release/v4.2 --recursive https://github.com/espressif/esp-idf.git
$ cd esp-idf
$ . $HOME/esp/esp-idf/install.sh
ERROR: This script was called from a virtual environment, can not create a virtual environment again
$ . ./export.sh 
Detecting the Python interpreter
Checking "python" ...
Python 3.8.5
"python" has been detected
Adding ESP-IDF tools to PATH...
Using Python interpreter in /home/harald/venv/bin/python
Checking if Python packages are up to date...
Python requirements from /home/harald/git/esp-idf/requirements.txt are satisfied.
Added the following directories to PATH:
  /home/harald/git/esp-idf/components/esptool_py/esptool
  /home/harald/git/esp-idf/components/espcoredump
  /home/harald/git/esp-idf/components/partition_table
  /home/harald/git/esp-idf/components/app_update
Done! You can now compile ESP-IDF projects.
Go to the project directory and run:

  idf.py build

You can ignore the error after executing . ./install.sh . All requirements should have been installed as expected into your virtual environment.

In case you get odd errors, delete ~/.espressif/ as it has the compiler tool chain. See also here if your code crash loops.

The AWS CLI tools need to be installed and configured (of course). Then finally the fun starts:

$ cd ~/git
$ git clone https://github.com/m5stack/Core2-for-AWS-IoT-EduKit.git
$ cd Core2-for-AWS-IoT-EduKit/Blinky-Hello-World/utilities/AWS_IoT_registration_helper/
$ pip install -r requirements.txt
$ python registration_helper.py -p /dev/ttyUSB0
[...lots of lines...]
Manifest was loaded successfully

That’ll throw an error if you use Python other than 3.7. The fix is simple: remove the bold lines in the registration_helper.py:

def check_environment():
    """Checks to ensure environment is set per AWS IoT EduKit instructions

    Verifies Miniconda is installed and the 'edukit' virtual environment
    is activated.
    Verifies Python 3.7.x is installed and is being used to execute this script.
    Verifies that the AWS CLI is installed and configured correctly. Prints
    AWS IoT endpoint address.
    """
    conda_env = os.environ.get('CONDA_DEFAULT_ENV')
    if conda_env == None or conda_env == "base":
        print("The 'edukit' Conda environment is not created or activated:\n  To install miniconda, visit https://docs.conda.io/en/latest/minico
nda.html.\n  To create the environment, use the command 'conda create -n edukit python=3.7'\n  To activate the environment, use the command 'con
da activate edukit'\n")
    print("Conda 'edukit' environment active...")
    
    if sys.version_info[0] != 3 or sys.version_info[1] != 7:
        print(f"Python version {sys.version}")
        print("Incorrect version of Python detected. Must use Python version 3.7.x. You might want to try the command 'conda install python=3.7'
.")
        exit(0)
    print("Python 3.7.x detected...")

    aws_iot_endpoint = subprocess.run(["aws", "iot", "describe-endpoint", "--endpoint-type", "iot:Data-ATS"], universal_newlines=True, capture_o
utput=True)
    if aws_iot_endpoint.returncode != 0:

Now in the AWS console you can find it:

and you can also get its endpoint from the AWS CLI:

$ aws iot describe-endpoint --endpoint-type iot:Data-ATS
{
    "endpointAddress": "c7xxxxxxxxxx0f-ats.iot.ap-northeast-1.amazonaws.com"
}

Use idf.py menuconfig to configure the endpoint name and the WiFi connectivity, then idf.py build flash monitor -p /dev/ttyUSB0 to build, flash and then connect to the serial port to watch it. Then you should see incoming MQTT messages:

And you make the LEDs blink by publishing into CLIENT_ID/blink. Stop blinking by publishing to the same topic. If you don’t know your CLIENT_ID, look it up on the display.

Given that AWS charges you for IoT traffic like those messages, don’t keep it messaging all day long. It only takes 5 days 18h only to hit the limit of free 500k messages to send (at 1/s).

Saleae Logic Analyzer – First Impressions

I bought myself a Saleae Logic 8 as I am constantly looking at oscilloscopes to help debugging problems with I/O on microcontrollers. It’s frustrating if you “see” nothing, beside you don’t get the results you want. Static signals are no problem: plug in an LED and you can see the state, but this does not work anymore as soon as frequencies larger than 10Hz are used. I2C runs at 100 or 400kHz…SPI even higher.

One fix would be a digital oscilloscope, but they are usually limited to 2 or 4 channels, with 4 channels being already expensive. Some can decode digital protocols too.

But there’s an alternative: Logic analyzers. And Saleae has a nice one; Logic 8: 8 channels, 100MHz digital and 10MHz analog sample rate, 8 channel. And for non-commercial use it’s 50% off!

Ordered one. 3 days later it arrived.

After few hours playing with it, it’s clear: I should have bought one much earlier. Seeing the I2C and SPI traffic or just pulses is so simple and so useful for debugging. Here an example:

1 is the I2C data channel, 2 is the clock. 3 is the same but recorded analog. 4 is the decoded I2C data and 5 is a decoder extension I wrote this afternoon for decoding the data for the PCF8583 I used here (in clock mode). This is easier and at the same time more useful than I thought.

PCF8583 and Espruino

Looking for a I2C device to test and play with it, I found an old PCF8583 board I bought a long time ago from Futurlec. I would have guess about 10 years ago. I remember it can handle 3.3V and has the needed pull-up resistors via jumpers. Perfect for testing!

Unexpectedly I found no library for that chip at http://www.espruino.com/modules/, but looking at the data sheet, it’s not really needed. Setting time and reading time is straightforward:

I2C1.setup({sda: D5, scl: D4});

pcf8583Addr=0x51;

function BCDToBinary(n) {
  return (n>>4)*10+(n&0x0f);
}
function binaryToBCD(n) {
  return ((n/10)<<4) + (n % 10);
}
function BCDToString(n) {
  return String.fromCharCode((n>>4)+48, (n&0x0f)+48);
}
    
function getPCFTime() {
  I2C1.writeTo(pcf8583Addr, 1);
  let d=I2C1.readFrom(pcf8583Addr, 4);
  return `${BCDToString(d[3])}:${BCDToString(d[2])}:${BCDToString(d[1])}.${BCDToString(d[0])}`;
}

function getPCFDate() {
  I2C1.writeTo(pcf8583Addr, 5);
  let d=I2C1.readFrom(pcf8583Addr, 2);
  let year=(2020+((d[0]&0xc0)>>6)).toString();
  let month=BCDToString(d[1] & 0x1f);
  let day=BCDToString(d[0] & 0x3f);
  return `${year}-${month}-${day}`;
}

function setPCFTime(h, m, s) {
  I2C1.writeTo(pcf8583Addr, [0, 0x80, 0, binaryToBCD(s), binaryToBCD(m), binaryToBCD(h)]);
  I2C1.writeTo(pcf8583Addr, 0, [0x00]);
}

function start(){
 g.clear();
 g.drawString("Starting...", 0, 0);
 g.flip(); 
}

var g = require("SSD1306").connect(I2C1, start, {height:64});
require("Font8x12").add(Graphics);
g.setFont8x12();

function updateDisplay() {
  g.clear();
  g.drawString(getPCFDate(), 0, 0);
  g.drawString(getPCFTime(), 0, 20);
  g.flip();
}

setInterval(updateDisplay, 1000);

By the way, the most amazing part of this test was that the included CR2032 Lithium battery still works after that many years.

openLRSng

Long time ago I purchased 3 of those pictured above. Originally for long distance remote control airplanes. I never used them as 2.4GHz took over and that worked for sufficient distance to my eyesight.

Where those modules can be used is for long range IoT uses though. LoRaWAN would be ok too but I don’t have any of those available. And for controlling servos, this receiver got everything out-of-the-box.

Setup

Software comes from the openLRSng repo, specifically the 433MHz release for hardware type 3: RX-3.hex and TX-3.hex from here. Yes, the receiver can be a transmitter, and for telemetry it’s even a requirement to send back data (e.g. battery or signal status).

Configuration is well described here and it uses a Chrome app for this. In order:

  • Connect the transmitter via an USB serial port converter (see Hardware Guide for pin-out details).
  • Flash one receiver with the receiver firmware RX-3.hex.
  • Flash one receiver with the transmitter firmware TX-3.hex.
  • Set up the transmitter to your liking.
  • Power up the receiver by connecting about 5V and GND to the receiver’s port 1. To force binding connect port 1 and 2 (the signal) via a jumper or a jumper cable. Should not be needed though. That power is used to power up the receiver and power the servos. It’s regulated down to 3.3V for the CPU.
  • Shortly after powering on the receiver it should bind to the transmitter and in the UI the receiver tab will be populated.
  • Set up all parameters (frequency) on the transmitter and save them in its EEPROM. Repeat for the receiver.

And you are done with the configuration work. From now on the two units don’t need a computer anymore: the transmitter will expect a CPPM (AKA PPMSum) input signal on port 5 and the receiver will have 8 channels of PWM data as output.

Now the part which took me longest: You have to create a CPPM signal on Port 5 on the transmitter (as per transmitter pin-out). That gets transmitted to the receiver which then shows servo-compatible PWM signals on its 8 ports (port 1 is used for RSS).

Since it’s quite time critical (it’s all about pulses in the 1000-2000µs range with ideally microsecond resolution and low jitter), a hardware timer solution is needed here. Luckily Arduino has PPMEncoder for exactly that. There was an unexpected snag though: the default of PPMEncoder is 500µs (0%) to 2500µs (100%) which causes the transmitter to not recognize the input CPPM signal anymore and then the receiver goes into “fail safe” mode (visible by the LEDs changing). If you vary the signal from 1000µs to 2000µs, all is well. In fact, 550..2450 works too. Since the initial pulse PPMEncoder creates is 500µs long, the total signal needs to be a bit more. However the servo I tested does not move smoothly outside the normal range: while it can move further left than 1000µs and further right than 2000µs, the movement is not linear anymore.

Here the rather simple Arduino program to create a CPPM signal on Pin 7:

#include "PPMEncoder.h"

#define OUTPUT_PIN 7

void setup() {
  ppmEncoder.begin(OUTPUT_PIN);
  for (int ch=0; ch<8; ++ch)
    ppmEncoder.setChannel(ch, 1500);
}

const int servoMax=2000;
const int servoMin=1000;

void loop() {
  while(1) {
  for (int i=servoMin; i<servoMax; ++i) {
    for (int ch=0; ch<4; ++ch)
      ppmEncoder.setChannel(ch, i);
    delay(20);
  }
  delay(500);
  for (int i=servoMax; i>=servoMin; i-=10) {
    for (int ch=2; ch<4; ++ch)
      ppmEncoder.setChannel(ch, i);
    delay(20);
  }
  delay(2000);
  }
}

A very boring video of the resulting servo movements is available here.

LEGO+ and M5Stack

When the Lego Mindstorm NXT came out, I bought one. Good fun, but I realized quite quickly that programming via GUI is…not fun at all. Attempts to program in NXC have been made, but it wasn’t as much fun as I hoped.

Enter the M5Stack with its LEGO+ module (now renamed to DC Motor) which can connect to the motors. Not the sensors, but there’s of course the way more useful M5Stack Units.

And since the M5Stack Core unit has WiFi, a display, buttons and can be programmed in microPython, it’s fun again!

However the latest firmware (v.1.7.2) misses the module for the LEGO+ module. v1.4.5 has it though.

Preview(opens in a new tab)

Task 1: Since you can only tell the motor to turn with a given direction and speed, make it so reach a certain amount of rotations. Classical PID control loop:

This rather simple program does the trick:

from m5stack import *
from m5ui import *
from uiflow import *
import module
import time

class PID:

    def __init__(self, P=0.2, I=0.0, D=0.0):
        self.Kp = P
        self.Ki = I
        self.Kd = D

        self.sample_time = 0.00
        self.current_time = time.ticks_ms()
        self.last_time = self.current_time
        self.clear()

    def clear(self):
        self.SetPoint = 0.0
        self.PTerm = 0.0
        self.ITerm = 0.0
        self.DTerm = 0.0
        self.last_error = 0.0

        # Windup Guard
        self.int_error = 0.0
        self.windup_guard = 10000.0

        self.output = 0.0

    def update(self, feedback_value):
        """Calculates PID value for given reference feedback
        .. math::
            u(t) = K_p e(t) + K_i \int_{0}^{t} e(t)dt + K_d {de}/{dt}
        .. figure:: images/pid_1.png
           :align:   center
           Test PID with Kp=1.2, Ki=1, Kd=0.001 (test_pid.py)
        """
        error = self.SetPoint - feedback_value

        self.current_time = time.ticks_ms()
        delta_time = self.current_time - self.last_time
        delta_error = error - self.last_error

        if (delta_time >= self.sample_time):
            self.PTerm = self.Kp * error
            self.ITerm += error * delta_time

            if (self.ITerm < -self.windup_guard):
                self.ITerm = -self.windup_guard
            elif (self.ITerm > self.windup_guard):
                self.ITerm = self.windup_guard

            self.DTerm = 0.0
            if delta_time > 0:
                self.DTerm = delta_error / delta_time

            # Remember last time and last error for next calculation
            self.last_time = self.current_time
            self.last_error = error

            self.output = self.PTerm + (self.Ki * self.ITerm) + (self.Kd * self.DTerm)
            lcd.print("PTerm: "+str(self.PTerm)+"    ", 0, 80, 0xffffff)
            lcd.print("ITerm: "+str(self.ITerm)+"    ", 0, 100, 0xffffff)
            lcd.print("DTerm: "+str(self.DTerm)+"    ", 0, 120, 0xffffff)

    def setWindup(self, windup):
        self.windup_guard = windup

lego_motor = module.get(module.LEGO)
setScreenColor(0x222222)

def buttonA_wasPressed():
  global speed
  lego_motor.M1.stop()
  pass
btnA.wasPressed(buttonA_wasPressed)

def buttonC_wasPressed():
  global speed
  lego_motor.M1.encode_clear()
  pass
btnC.wasPressed(buttonC_wasPressed)

speed = 0
lego_motor.M1.set_pwm(speed)

# Parameter tuned for motor@9V
pid=PID(0.3, 0.0, 40.0)

# Turn 1/8th of circle, that's 6 full rotations for the motor
pid.SetPoint=360*6
lego_motor.M1.encode_clear();

# If motor signal is less than about 50
# the motor won't turn. So stop then

too_small=50
too_small_count_max=20
too_small_count=0

for count in range(1000):
    pid.update(lego_motor.M1.encoder_read())
    speed=pid.output;
    lcd.print("Encoder: "+str(lego_motor.M1.encoder_read())+"      ", 0, 0, 0xffffff)
    lcd.print("Speed: "+str(speed)+" ", 0, 20, 0xffffff)
    lcd.print("Time: "+str(time.ticks_ms()), 0, 40, 0xffffff)
    if speed < -255:
        speed = -255
    if speed > 255:
        speed = 255
    lego_motor.M1.set_pwm(speed)
    if speed < too_small:
        too_small_count += 1
    else:
        too_small_count = 0
    if too_small_count > too_small_count_max:
        break;
    time.sleep(0.2)

lego_motor.M1.stop()

It was interesting to find usable PID parameters. In the end https://en.wikipedia.org/wiki/PID_controller#Ziegler%E2%80%93Nichols_method worked quite well. The motor has a lot of play and does not work at lowest speeds. I’ll instead use some servos I still have and see what I can PID with those.

Create your website with WordPress.com
Get started