I previously covered how to run your own ChatGPT script with Python and Golang. But what if you want to create a script that automatically runs the ChatGPT generated code? That is what I will cover in this post. The idea is really simple:
Create a script that asks for user input
Pass the input to ChatGPT
Run the ChatGPT generated code
NOTE: Please read the caveats at the end of the post before using this!
Calling The OpenAI API
First step is simple, just call the OpenAI API with the user input. If "-v" is given as the first argument, print the raw answer and usage statistics as well.
#!/usr/bin/python3import openaiimport sysopenai.api_key = 'sk-xxx'verbose = sys.argv[1] == '-v'prompt = ' '.join(sys.argv[2 if verbose else 1:])resp = openai.ChatCompletion.create( model="gpt-3.5-turbo", messages=[ {"role": "system", "content": "You are Python code generator. Answer with just the Python code."}, {"role": "user", "content": prompt}, ])data = resp['choices'][0]['message']['content']if verbose: print(data, 'Usage was:', resp['usage'], sep='\n')
Parsing the Code From Response
A rather simplistic implementation looks for set of start tokens and end tokens and returns the code between them. This is not perfect, but it works as long as the ChatGPT response does not contain multiple code blocks.
OpenAI came out with ChatGPT, and wow, that is quite something. What is also remarkable is the
load the ChatGPT client is under, and how often it is "experiencing high demand".
Or just requires you to prove you are human and log in again.
You can get ChatGPT Plus for $20 a month, but hey, you can also get chat experience for $0.002 per 1000 tokens. To hit that monthly fee, you need to use 10 M tokens, which is not that far from 10 M words. That is pretty heavy use...
Using OpenAI ChatGPT (gpt-3.5-turbo) through Python API
To use the ChatGPT API, at its simplest form with Python3 you just pip install openai and create a short script:
#!/usr/bin/python3import openaiimport sysopenai.api_key = 'sk-yourkeyhere'if len(sys.argv) < 2: prompt = input('Prompt: ')else: prompt = ' '.join(sys.argv[1:])resp = openai.ChatCompletion.create( model="gpt-3.5-turbo", messages=[ {"role": "system", "content": "You are a programming expert giving advice to a colleague."}, {"role": "user", "content": prompt} ])print(resp['choices'][0]['message']['content'])print('Usage was', resp['usage'])
You need to create credentials at OpenAI platform, enter your credit card and set a warning and hard treshold for monthly billing (I set mine to $4 and $10, respectively). But after filling your API key to the script, you can just run it:
$ python3 chat.py What is the capital of AlaskaThe capital of Alaska is Juneau. However, I believe you were looking for programming advice. What specifically are you working on and what kind of advice are you seeking?Usage was { "completion_tokens": 34, "prompt_tokens": 30, "total_tokens": 64}
Now that is pretty nice, but we can do better!
Golang client with SvelteKit frontend
In my previous Golang+SvelteKit GUI post I explored how to create a Go application acting as a web server and making a user interface with SvelteKit:
Golang has high performance and excellent set of libraries to accomplish many tasks
Cross-platform support out of the box with compact executables
SvelteKit is fast to develop as a frontend, requiring very low amount of code for rich interactive UIs
OpenAI does not produce it's own Go library, but that API as well documented and shabaronov has made an excellent Golang OpenAI API library that makes calling the API simple. It even supports GPT4, so if you have access to that, you can create a GPT4 chat client as well.
Without further ado, here's the Github repository for my GoChatGPT client. You can basically git clone://github.com/jokkebk/gochatgpt and follow the instructions in README.md to get it running, it's about 4 commands all in all.
Let's look a bit what the code does!
Golang ChatGPT Backend
Main method of the backend is nothing too complex:
Serve the SvelteKit GUI from static folder (including index.html when user requests /).
Have a chat endpoint at /chat that takes a JSON object with chat messages and passes it to OpenAI API.
Return the OpenAI [ChatGPT] response as a string to calling client.
Compared to Arduino Uno, the RP2040 has major advantages for this project:
Much higher clock frequency of 133 MHz means there's cycles to spare even at ~1 Mhz rates
Relatively vast SRAM memory, 264 kB vs. 2 kB
Native C SDK that is rather easy to work with
I'm using the Seeed XIAO RP2040 for this project. It is extremely compact and has a nice USB-C interface. You can see the wiring, it's just 3.3V and GND to the receiver (which luckily did work fine with that voltage) and signal to GPIO pin 0.
Note that while RP2040 pinout has 5V supply line, the GPIO pins are not 5V tolerant, so you should not power a 5V receiver and directly connect it to pin 0. A voltage divider is strongly recommended to avoid longer term damage to the RP2040 pin.
Setting up RP2040 programming environment
I basically followed the Getting started guide that was linked from the Pico SDK Github to get the Blink example working. After that, it was quite simple to set up a new project following the "Quick-start your own project", setting up CMakeLists.txt like this:
cmake_minimum_required(VERSION 3.13)# initialize the SDK based on PICO_SDK_PATH# note: this must happen before project()include(pico_sdk_import.cmake)project(joonas-pico)# initialize the Raspberry Pi Pico SDKpico_sdk_init()# rest of your projectadd_subdirectory(logic_analyze)
$ mkdir logic_analyze$ cd logic_analyze$ wget https://codeandlife.com/images/2023/logic-analyze-pico.zip$ unzip logic_analyze-pico.zip$ mkdir build$ cd build$ export PICO_SDK_PATH=../../pico-sdk$ cmake ..$ make
Note that this assumes you placed the example directory logic_analyze alongside your pico-sdk directory.
After running make, you should find the logic.uf2 file under logic_analyze directory and you can just drag and drop it to your RP2040 when it is in USB drive mode.
C Code for Recording GPIO Changes
The code is basically combination of what I did for Arduino and Raspberry Pi, and the hello_gpio_irq and hello_timer examples. Basic logic:
Setup stdio_init_all() (over USB, necessary definitions to enable that in CMakeLists.txt file) and wait until stdio_usb_connected() returns true.
Loop forever, asking the user over serial (USB) to press a key to start recording
Clear receive buffer
Set alarm timeout of 5 seconds to end recording if buffer hasn't been filled
Set up GPIO interrupt triggers on rising and falling edges of pin 0
In the interrupt handler, record time elapsed since last edge using time_us_64()
Once timeout is reached or buffer has been filled, disable GPIO interrupt and print out received timings.
The security of sensitive information is of utmost importance today. One way to enhance the security of
stored passwords is by using PBKDF2 with SHA256 HMAC, a cryptographic algorithm that adds an extra layer of protection if the password hashes are compromised. I covered recently how to calculate PBKDF2 yourself with Python, but today needed to do the same with Javascript.
CryptoJS documentation on PBKDF2 is as scarce as everything on this library, and trying out the 256 bit key example with Node.js gives the following output:
$ nodeWelcome to Node.js v19.6.0.Type ".help" for more information.> const crypto = require('crypto-js')undefined> const key = crypto.PBKDF2("password", "salt", {keySize: 256/32, iterations: 4096})undefined> crypto.enc.Hex.stringify(key)'4b007901b765489abead49d926f721d065a429c12e463f6c4cd79401085b03db'
Now let's recall what Python gave here:
$ pythonPython 3.10.7 (tags/v3.10.7:6cc6b13, Sep 5 2022, 14:08:36) [MSC v.1933 64 bit (AMD64)] on win32Type "help", "copyright", "credits" or "license" for more information.>>> import hashlib>>> hashlib.pbkdf2_hmac('sha256', b'password', b'salt', 4096).hex()'c5e478d59288c841aa530db6845c4c8d962893a001ce4e11a4963873aa98134a'>>>
Uh-oh, they don't match! Now looking at pbkdf2.js in the CryptoJS Github source, we can see that the algorithm defaults to SHA-1:
Seeing how keySize and iterations are overridden, we only need to locate the SHA256 in proper namespace to guide the implementation to use SHA256 HMAC instead:
$ nodeWelcome to Node.js v19.6.0.Type ".help" for more information.> const crypto = require('crypto-js')undefined> const key = crypto.PBKDF2("password", "salt", {keySize: 256/32,... iterations: 4096, hasher: crypto.algo.SHA256})undefined> crypto.enc.Hex.stringify(key)'c5e478d59288c841aa530db6845c4c8d962893a001ce4e11a4963873aa98134a'
Awesome! Now we are ready to rock'n'roll with a proper hash function.
A quick one tonight. Having just spent enjoyable hacking time to reverse engineer Chris Veness' AES 256 encryption library enough to be able to decrypt some old data I had using CryptoJS, I thought I will share it with the world to enjoy.
Now Chris' library is nice and simple, you can just encrypt stuff with AES 256 counter mode with a single line of code:
> AESEncryptCtr('Well this is fun!', 'password', 256)'SdzeY4GBgYHDEWay4JdHr/CnwwnAoBfjQA=='
Now AES 256 is a super standard cipher, so it should be pretty easy to decrypt that with another libray, right?
WRONG!
Wrong, standard AES crypto is not always easy to decrypt
Turns out that Chris' library does not use the 'password' in a way most other libraries use it, but instead chooses to:
Decode the string into UTF8
Create a 16 byte array and put the decoded string into beginning of the array
Initialize AES encryption ("key expansion") using this array
Encrypt a copy of the array as a single AES block using the key expansion (that the library has internally just been initialized with)
Expand the AES encrypted 16 bytes by doubling the array into 32 byte one
Use the 32 byte array as the AES key
Now if you think that any other library would use the same method, you would be dead wrong. Also, most libraries do not expose the same set of functions to replicate this process in any simple manner.
To add insult to injury, JavaScript has so poor support for byte data that it seems each crypto library uses its own internal representation of binary data. For example, CryptoJS likes to make a uint32 array, so [1, 2, 3, 4, 5, 6, 7, 8] is represented as {words: [0x01020304, 0x05060708], sigBytes: 8}!
Thankfully, yours truly is a true master and after just 2 hours of trial and error, I managed to produce this golden nugget:
const crypto = require('crypto-js');function VanessKey(password) { const iv = { words: [0,0,0,0], sigBytes: 16}; const encrypted = crypto.AES.encrypt( crypto.enc.Utf8.parse(password.padEnd(16, '\0')), crypto.enc.Utf8.parse(password.padEnd(32, '\0')), { iv } ).ciphertext; const ew = encrypted.words.slice(0, 4); // double the first 4 words of the encrypted encrypted.words = ew.concat(ew); return encrypted;}const key = VanessKey('password');console.log(key);console.log(crypto.enc.Hex.stringify(key));
Saving it as decrypt.cjs and running node decrypt.cjs should produce the "Vaness key" for 'password':
Now we only need the magic incantation to decrypt the Base64 encoded data SdzeY4GBgYHDEWay4JdHr/CnwwnAoBfjQA== produced in the intro! This is also pretty non-straightforward:
Finale time! After analyzing the Nexa 433 MHz smart power plug remote control signal with Arduino Uno
and regular USB soundcard, it is time to try some heavier guns: Raspberry Pi 4 and
PicoScope 2208B MSO.
I was initially sceptical on using a Raspberry Pi for analyzing signals, due to several reasons:
Most GPIO projects on RaspPi seem to use Python, which is definitely not a low-latency solution, especially compared to raw C.
Having done a raw C GPIO benchmark on RaspPi in the past, the libraries were indeed quite... low level.
I had serious doubts that a multitasking operating system like Linux running on the side of time-critical signal measurements might impact performance (it is not a RTOS after all).
However, there are projects like rpi-rf that seem to work, so I dug in and found out some promising aspects:
There is a interrupt driven GPIO edge detection capability in the RaspPi.GPIO library that should trigger immediately on level changes.
Python has a sub-microsecond precision time.perf_counter_ns() that is suitable for recording the time of the interrupt
Python code to record GPIO signals in Raspberry Pi
While rpi-rf did not work properly with my Nexa remote, taking hints from the implementation allowed me to write pretty concise Python script to capture GPIO signals:
from RPi import GPIOimport time, argparseparser = argparse.ArgumentParser(description='Analyze RF signal for Nexa codes')parser.add_argument('-g', dest='gpio', type=int, default=27, help='GPIO pin (Default: 27)')parser.add_argument('-s', dest='secs', type=int, default=3, help='Seconds to record (Default: 3)')parser.add_argument('--raw', dest='raw', action='store_true', default=False, help='Output raw samples')args = parser.parse_args()times = []GPIO.setmode(GPIO.BCM)GPIO.setup(args.gpio, GPIO.IN)GPIO.add_event_detect(args.gpio, GPIO.BOTH, callback=lambda ch: times.append(time.perf_counter_ns()//1000))time.sleep(args.secs)GPIO.remove_event_detect(args.gpio)GPIO.cleanup()# Calculate difference between consecutive timesdiff = [b-a for a,b in zip(times, times[1:])]if args.raw: # Print a raw dump for d in diff: print(d, end='\n' if d>5e3 else ' ')
The code basically parses command line arguments (defaulting to GPIO pin 27 for input) and sets up GPIO.add_event_detect() to record (microsecond) timings of the edge changes on the pin. In the end, differences between consecutive times will be calculated to yield edge lengths instead of timings.
Raspberry Pi is nice also due to the fact that it has 3.3V voltage available on GPIO. Wiring up the 433 MHz receiver was a pretty elegant matter (again, there is a straight jumper connection between GND and last pin of the receiver):
Wire the receiver in
Start the script with python scan.py --raw (use -h option instead for help on command)
Press the Nexa remote button 1 during the 3 second recording interval
Here's how the output looks like (newline is added after delays longer than 5 ms):