Information wants to be free...

Space Ship Simulator in OpenGL

I finally finished a project I had suspended many years ago when I was looking into OpenGL. At that time my solution had problems that I now later have discovered was related to the dreaded Gimbal lock caused by the use of Euler angles. The new solution uses rotation matrices directly. It uses the GLUT library which is kind of deprecated, but still very widely available and convenient for simpler OpenGL programs and demos like this one.

This space ship simulator is quite simple, but allows movement in all directions. When the program is started, the "universe" is populated with some randomly generated (really small) planets which can be navigated around. Use W/S to pitch, A/S to yaw, Z/C to roll and R/F to increase/decrease thrusters.

Here is the code, compile it like so: gcc -o space space.c -lGL -lGLU -lglut -lm -Wall

#include <GL/glut.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>

#define WORLD_SIZE 25 /* Distance to fake walls from origin. */

#define GRID_SPACING 5
#define GRID_START ((0 - WORLD_SIZE) + GRID_SPACING)
#define GRID_END (WORLD_SIZE - GRID_SPACING)
#define GRID_SIZE ((GRID_END - GRID_START) / GRID_SPACING)

#define PLANETS_MAX 10
#define PLANETS_CHANCE (GRID_SIZE * GRID_SIZE * GRID_SIZE) / PLANETS_MAX



typedef struct vector_s {
  float x;
  float y;
  float z;
} vector_t;

typedef struct planet_s {
  vector_t pos;
  float size;
  float color_r;
  float color_g;
  float color_b;
} planet_t;

typedef struct planet_distance_s {
  int planet_index;
  float distance;
} planet_distance_t;



static int camera_mode = 0;
static float ship_speed = 0.0;
static int old_mouse_x;
static int old_mouse_y;

static planet_t planets[PLANETS_MAX];
static int planets_count = 0;

static vector_t ship_forward_vector = {1,0,0};
static vector_t ship_up_vector      = {0,1,0};
static vector_t ship_right_vector   = {0,0,1};
static vector_t ship_position       = {0,0,0};



static vector_t vector_normalize(vector_t v)
{
  float magnitude;
  vector_t normalized;

  magnitude = sqrt((v.x * v.x) + (v.y * v.y) + (v.z * v.z));
  normalized.x = v.x / magnitude;
  normalized.y = v.y / magnitude;
  normalized.z = v.z / magnitude;

  return normalized;
}

static vector_t vector_cross_product(vector_t a, vector_t b)
{
  vector_t cross;

  cross.x = (a.y * b.z) - (a.z * b.y);
  cross.y = (a.z * b.x) - (a.x * b.z);
  cross.z = (a.x * b.y) - (a.y * b.x);

  return cross;
}

static vector_t vector_multiply(vector_t v, float f)
{
  vector_t product;

  product.x = v.x * f;
  product.y = v.y * f;
  product.z = v.z * f;

  return product;
}

static vector_t vector_sum(vector_t a, vector_t b)
{
  vector_t sum;

  sum.x = a.x + b.x;
  sum.y = a.y + b.y;
  sum.z = a.z + b.z;

  return sum;
}

static vector_t vector_difference(vector_t a, vector_t b)
{
  vector_t difference;

  difference.x = a.x - b.x;
  difference.y = a.y - b.y;
  difference.z = a.z - b.z;

  return difference;
}

static float vector_distance(vector_t a, vector_t b)
{
  return sqrt(((a.x - b.x) * (a.x - b.x)) +
              ((a.y - b.y) * (a.y - b.y)) +
              ((a.z - b.z) * (a.z - b.z)));
}



static void ship_pitch(float angle)
{
  ship_forward_vector = vector_normalize(
    vector_sum(
      vector_multiply(ship_forward_vector, cos(angle * (M_PI / 180))),
      vector_multiply(ship_up_vector, sin(angle * (M_PI / 180)))));

  ship_up_vector = vector_cross_product(
    ship_forward_vector, ship_right_vector);

  ship_up_vector = vector_multiply(ship_up_vector, -1.0);
}

static void ship_yaw(float angle)
{
  ship_forward_vector = vector_normalize(
    vector_difference(
      vector_multiply(ship_forward_vector, cos(angle * (M_PI / 180))),
      vector_multiply(ship_right_vector, sin(angle * (M_PI / 180)))));

  ship_right_vector = vector_cross_product(
    ship_forward_vector, ship_up_vector);
}

static void ship_roll(float angle)
{
  ship_right_vector = vector_normalize(
    vector_sum(
      vector_multiply(ship_right_vector, cos(angle * (M_PI / 180))),
      vector_multiply(ship_up_vector, sin(angle * (M_PI / 180)))));

  ship_up_vector = vector_cross_product(
    ship_forward_vector, ship_right_vector);

  ship_up_vector = vector_multiply(ship_up_vector, -1.0);
}

static void ship_advance(float distance)
{
  ship_position = vector_sum(ship_position, 
    vector_multiply(ship_forward_vector, distance));
}

#ifdef DEBUG_MODE
static void ship_ascend(float distance)
{
  ship_position = vector_sum(ship_position, 
    vector_multiply(ship_up_vector, distance));
}

static void ship_strafe(float distance)
{
  ship_position = vector_sum(ship_position, 
    vector_multiply(ship_right_vector, distance));
}
#endif /* DEBUG_MODE */



static void idle()
{
  if (ship_speed != 0) {
    ship_advance(ship_speed);
  }

  glutPostRedisplay();

#ifdef DEBUG_MODE
  fprintf(stderr, "Speed: %.2f, X: %.2f, Y: %.2f, Z: %.2f\n",
    ship_speed, ship_position.x, ship_position.y, ship_position.z);
#endif /* DEBUG_MODE */
}

static void draw_ship(void)
{
  float rotation[16];

  glTranslatef(ship_position.x, ship_position.y, ship_position.z);

  rotation[0]  = ship_right_vector.x;
  rotation[1]  = ship_right_vector.y;
  rotation[2]  = ship_right_vector.z;
  rotation[3]  = 0;
  rotation[4]  = ship_up_vector.x;
  rotation[5]  = ship_up_vector.y;
  rotation[6]  = ship_up_vector.z;
  rotation[7]  = 0;
  rotation[8]  = ship_forward_vector.x;
  rotation[9]  = ship_forward_vector.y;
  rotation[10] = ship_forward_vector.z;
  rotation[11] = 0;
  rotation[12] = 0;
  rotation[13] = 0;
  rotation[14] = 0;
  rotation[15] = 1;
  glMultMatrixf(rotation);

  glScalef(0.5, 0.5, 0.5);
  glColor3f(0.9, 0.9, 0.9);

  glBegin(GL_LINE_LOOP);
    glVertex3f(-1.0, -1.0, 0.0);
    glVertex3f(1.0, -1.0, 0.0);
    glVertex3f(1.0, 1.0, 0.0);
    glVertex3f(-1.0, 1.0, 0.0);
  glEnd();
  glBegin(GL_LINE_LOOP);
    glVertex3f(-1.0, -1.0, 0.0);
    glVertex3f(1.0, -1.0, 0.0);
    glVertex3f(0.0, 0.0, 4.0);
  glEnd();
  glBegin(GL_LINE_LOOP);
    glVertex3f(1.0, -1.0, 0.0);
    glVertex3f(1.0, 1.0, 0.0);
    glVertex3f(0.0, 0.0, 4.0);
  glEnd();
  glBegin(GL_LINE_LOOP);
    glVertex3f(1.0, 1.0, 0.0);
    glVertex3f(-1.0, 1.0, 0.0);
    glVertex3f(0.0, 0.0, 4.0);
  glEnd();
  glBegin(GL_LINE_LOOP);
    glVertex3f(-1.0, 1.0, 0.0);
    glVertex3f(-1.0, -1.0, 0.0);
    glVertex3f(0.0, 0.0, 4.0);
  glEnd();
}

#ifdef DEBUG_MODE
static void draw_axis_indicator(void)
{
  /* Draw it at origin. */
  GLfloat old_line_width;
  glGetFloatv(GL_LINE_WIDTH, &old_line_width);
  glLineWidth(3.0);

  glBegin(GL_LINES);
    /* Red X-Axis */
    glColor3f(1, 0, 0);
    glVertex3f(0, 0, 0);
    glVertex3f(1, 0, 0); 
    /* Green Y-Axis */
    glColor3f(0, 1, 0);
    glVertex3f(0, 0, 0);
    glVertex3f(0, 1, 0);
    /* Blue Z-Axis */
    glColor3f(0, 0, 1);
    glVertex3f(0, 0, 0);
    glVertex3f(0, 0, 1); 
  glEnd();

  glLineWidth(old_line_width);
}
#endif /* DEBUG_MODE */

static int planet_distance_compare(const void *p1, const void *p2)
{
  return ((planet_distance_t *)p1)->distance <
         ((planet_distance_t *)p2)->distance;
}

static void display(void)
{
  glClear(GL_COLOR_BUFFER_BIT);
  glLoadIdentity();

  if (camera_mode == 0) {
    /* Inside ship. */
    vector_t view_point;
    view_point = vector_sum(ship_position, ship_forward_vector);

    gluLookAt(ship_position.x, ship_position.y, ship_position.z,
              view_point.x, view_point.y, view_point.z,
              ship_up_vector.x, ship_up_vector.y, ship_up_vector.z);
  } else {
    /* Outside ship, a static look towards origin. */
    glRotatef(45.0, 1.0, 0.0, 0.0);
    glRotatef(120.0, 0.0, 1.0, 0.0);
    glTranslatef(WORLD_SIZE / 2, 0 - (WORLD_SIZE / 2), WORLD_SIZE / 2);
  }

  /* Draw floor/roof/walls, which are actually big cubes! */
  glColor3f(0.0, 0.0, 0.0);

  glPushMatrix();
    glTranslatef(0.0, -WORLD_SIZE, 0.0);
    glScalef(WORLD_SIZE * 2, 0.1, WORLD_SIZE * 2);
#ifdef DEBUG_MODE
    glColor3f(0.0, 0.5, 0.0);
#endif /* DEBUG_MODE */
    glutSolidCube(1.0);
  glPopMatrix();

  glPushMatrix();
    glTranslatef(0.0, WORLD_SIZE, 0.0);
    glScalef(WORLD_SIZE * 2, 0.1, WORLD_SIZE * 2);
#ifdef DEBUG_MODE
    glColor3f(0.0, 0.0, 0.5);
#endif /* DEBUG_MODE */
    glutSolidCube(1.0);
  glPopMatrix();

  glPushMatrix();
    glTranslatef(WORLD_SIZE, 0.0, 0.0);
    glScalef(0.1, WORLD_SIZE * 2, WORLD_SIZE * 2);
#ifdef DEBUG_MODE
    glColor3f(0.5, 0.0, 0.0);
#endif /* DEBUG_MODE */
    glutSolidCube(1.0);
  glPopMatrix();

  glPushMatrix();
    glTranslatef(-WORLD_SIZE, 0.0, 0.0);
    glScalef(0.1, WORLD_SIZE * 2, WORLD_SIZE * 2);
#ifdef DEBUG_MODE
    glColor3f(0.5, 0.5, 0.0);
#endif /* DEBUG_MODE */
    glutSolidCube(1.0);
  glPopMatrix();

  glPushMatrix();
    glTranslatef(0.0, 0.0, WORLD_SIZE);
    glScalef(WORLD_SIZE * 2, WORLD_SIZE * 2, 0.1);
#ifdef DEBUG_MODE
    glColor3f(0.5, 0.0, 0.5);
#endif /* DEBUG_MODE */
    glutSolidCube(1.0);
  glPopMatrix();

  glPushMatrix();
    glTranslatef(0.0, 0.0, -WORLD_SIZE);
    glScalef(WORLD_SIZE * 2, WORLD_SIZE * 2, 0.1);
#ifdef DEBUG_MODE
    glColor3f(0.0, 0.5, 0.5);
#endif /* DEBUG_MODE */
    glutSolidCube(1.0);
  glPopMatrix();

  /* Calculate distance from ship to planets, because they need to be
     drawn/rendered from the furthest away first. */
  planet_distance_t distance[PLANETS_MAX];
  for (int i = 0; i < planets_count; i++) {
    distance[i].planet_index = i;
    distance[i].distance = vector_distance(ship_position, planets[i].pos);
  }
  qsort(distance, planets_count, sizeof(planet_distance_t),
    planet_distance_compare);

  /* Draw planets. */
  for (int i = 0; i < planets_count; i++) {
    int n = distance[i].planet_index;
    glPushMatrix();
      GLUquadric *quadric = gluNewQuadric();
      if (quadric != 0) {
        glTranslatef(planets[n].pos.x, planets[n].pos.y, planets[n].pos.z);
        glColor3f(planets[n].color_r, planets[n].color_g, planets[n].color_b);
        gluSphere(quadric, planets[n].size, 20, 20);
        gluDeleteQuadric(quadric);
      }
    glPopMatrix();
  }
    
  if (camera_mode != 0) {
    glPushMatrix();
      draw_ship();
    glPopMatrix();
  }

#ifdef DEBUG_MODE
  glPushMatrix();
    draw_axis_indicator();
  glPopMatrix();
#endif /* DEBUG_MODE */

  glFlush();
  glutSwapBuffers();
}

static void reshape(int w, int h)
{
  glViewport(0, 0, (GLsizei)w, (GLsizei)h);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glFrustum(-1.0, 1.0, -1.0, 1.0, 1.5, 150.0);
  glMatrixMode(GL_MODELVIEW);
}

static void keyboard(unsigned char key, int x, int y)
{
  switch (key) {
  case 's':
    ship_pitch(-1.0);
    break;
  case 'w':
    ship_pitch(1.0);
    break;
  case 'd':
    ship_yaw(-1.0);
    break;
  case 'a':
    ship_yaw(1.0);
    break;
  case 'z':
    ship_roll(-1.0);
    break;
  case 'c':
    ship_roll(1.0);
    break;
  case 'r':
    ship_speed += 0.01;
    break;
  case 'f':
    ship_speed -= 0.01;
    break;
#ifdef DEBUG_MODE
  case 't':
    ship_advance(0.1);
    break;
  case 'g':
    ship_advance(-0.1);
    break;
  case 'y':
    ship_ascend(0.1);
    break;
  case 'h':
    ship_ascend(-0.1);
    break;
  case 'u':
    ship_strafe(0.1);
    break;
  case 'j':
    ship_strafe(-0.1);
    break;
  case 'x':
    camera_mode = (camera_mode) ? 0 : 1;
    break;
#endif /* DEBUG_MODE */
  case 'q':
    exit(0);
    break;
  }
}

static void motion(int x, int y)
{
  if (x > old_mouse_x) {
    ship_yaw(1.0);
  } else if (x < old_mouse_x) {
    ship_yaw(-1.0);
  }

  if (y > old_mouse_y) {
    ship_pitch(-1.0);
  } else if (y < old_mouse_y) {
    ship_pitch(1.0);
  }

  old_mouse_x = x;
  old_mouse_y = y;
}

void generate_planets(void)
{
  for (int x = GRID_START; x < GRID_END; x += GRID_SPACING) {
    for (int y = GRID_START; y < GRID_END; y += GRID_SPACING) {
      for (int z = GRID_START; z < GRID_END; z += GRID_SPACING) {
        if (x == 0 && y == 0 && z == 0) {
          /* Ignore origin. */
          continue;
        }

        if ((rand() % PLANETS_CHANCE) == 0) {
          planets[planets_count].pos.x = (x - 2) + (rand() % 4);
          planets[planets_count].pos.y = (y - 2) + (rand() % 4);
          planets[planets_count].pos.z = (z - 2) + (rand() % 4);
          planets[planets_count].size = 
            ((rand() % ((GRID_SPACING * 10) - 1)) / 20.0) + 0.1;
          planets[planets_count].color_r = (rand() % 11) / 10.0;
          planets[planets_count].color_g = (rand() % 11) / 10.0;
          planets[planets_count].color_b = (rand() % 11) / 10.0;

#ifdef DEBUG_MODE
          printf("X=%.1f, Y=%.1f, Z=%.1f, Size=%.1f, Col=%.1f;%.1f;%.1f\n", 
            planets[planets_count].pos.x,
            planets[planets_count].pos.y,
            planets[planets_count].pos.z,
            planets[planets_count].size,
            planets[planets_count].color_r,
            planets[planets_count].color_g,
            planets[planets_count].color_b);
#endif /* DEBUG_MODE */

          if ((planets_count + 1) >= PLANETS_MAX) {
            return;
          } else {
            planets_count++;
          }
        }
      }
    }
  }
}

int main(int argc, char *argv[])
{
  srand(time(NULL));
  generate_planets();
  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB);
  glutInitWindowSize(800, 800);
  glutInitWindowPosition(100, 100);
  glutCreateWindow(argv[0]);
  glClearColor(0.0, 0.0, 0.0, 0.0);
  glShadeModel(GL_FLAT);
  glutDisplayFunc(display);
  glutReshapeFunc(reshape);
  glutIdleFunc(idle);
  glutKeyboardFunc(keyboard);
  glutMotionFunc(motion);
  glutMainLoop();
  return 0;
}
          


Here is a screenshot of what it looks like, just some "planets" that looks like spheres:

Space Ship Simulator Screenshot


Topic: Scripts and Code, by Kjetil @ 05/03-2021, Article Link

Turning Frog VL6180 Upgrade

Many years ago I got a soldering kit, the "ELENCO 21-882" which is a "frog robot" with two motors that reacts to sound. I had been thinking about ways to upgrade it, and I finally got around to doing that by controlling it with a Arduino Pro Mini and a VL6180 sensor to measure distance.

The original sound based control works by clocking a "4017 Decade Counter" IC with the input from a electret microphone. The easiest way to hijack the control is to remove this IC from the socket and wire the Arduino into it.

I used the following connections:
Arduino RAW <-> Socket pin 16 (VDD, 9V supply from battery.)
Arduino GND <-> Socket pin 8 (VSS)
Arduino D10 <-> Socket pin 1 (Output, diode for left motor.)
Arduino D11 <-> Socket pin 7 (Output, diode for right motor.)
Arduino D12 <-> Socket pin 2 (Output, diodes for both motors.)

Cobbled together it looks like this:

Frog Upgrade

The FTDI is only used for uploading the program and can be removed of course.

The Arduino Sketch code is very simple:

#include "Adafruit_VL6180X.h"

static Adafruit_VL6180X vl = Adafruit_VL6180X();
static int keep_going = 0;

void setup() {
  pinMode(10, OUTPUT); // Left motor
  pinMode(11, OUTPUT); // Right motor
  pinMode(12, OUTPUT); // Both motors
  pinMode(13, OUTPUT); // On-board Arduino LED
  vl.begin();
}

void loop() {
  uint8_t range = vl.readRange();
  uint8_t status = vl.readRangeStatus();

  if ((status == VL6180X_ERROR_NONE) && (range < 200)) {
    // Go left
    digitalWrite(12, LOW);
    digitalWrite(10, HIGH);
    keep_going = 10; // Keep going to finish the turn...
  } else {
    if (keep_going > 0) {
      keep_going--;
    } else {
      // Go forward
      digitalWrite(10, LOW);
      digitalWrite(12, HIGH);
    }
  }
  
  delay(50);
}
          


Topic: Scripts and Code, by Kjetil @ 13/02-2021, Article Link

USB IR REMOCON Firmware Replacement

I bought this Bit Trade One USB Infrared Remote Control Kit at an electronics store in Japan, which lets you use a standard TV remote to control a PC. While the device works on Linux, since it presents itself as a standard USB HID device, it can only be configured in Windows. But a bigger problem is that the original firmware contained some kind of bug where part of the TV remote mappings would get corrupted after power cycling.

To get around these limitations and problems I have created a new firmware replacement for the onboard PIC18F14K50 microcontroller. For maximum flexibility I decided to make something that emulates the protocol used by the Dangerous Prototypes USB IR Toy. This means the device can be used together with LIRC on Linux, which offers a lot of advanced functionality. This also moves the configuration aspect to software on Linux instead of needing to re-flash the PIC microcontroller EEPROM. Note that this device also has an IR sender, but I only implemented support for the IR receive part.

You can get the firmware here or the MPLAB project source code here. I have used Microchip's own USB stack for this project, which is licensed under the Apache license. The parts I wrote myself I consider to be released unlicensed directly into the public domain. I'm including the main part of the code here for an easy reference:

#include "system.h"

#include <stdint.h>
#include <string.h>
#include <stddef.h>

#include "usb.h"

#include "app_irtoy_emu.h"
#include "usb_config.h"

static uint8_t readBuffer[CDC_DATA_OUT_EP_SIZE];
static uint8_t writeBuffer[CDC_DATA_IN_EP_SIZE];

static bool inSamplingMode = false;
static uint16_t pulseHighLen = 0;
static uint16_t pulseLowLen = 0;
static uint16_t pendingPulse = 0;
static bool endOfTransmission = true;
static bool waitForEOT = false;

void samplingModeSetup(void)
{
    /* Set variables to known state. */
    pulseHighLen = 0;
    pulseLowLen = 0;
    pendingPulse = 0;
    endOfTransmission = true;
    waitForEOT = false;

    T0CON = 0b11000000; /* Enable Timer0 as 8-Bit with no scaling. */

    RCONbits.IPEN = 1; /* Enable priority levels on interrupts. */
    INTCON2bits.TMR0IP = 1; /* Make Timer0 a high priority interrupt. */
    INTCONbits.TMR0IF = 0; /* Clear Timer0 interrupt flag. */
    INTCONbits.TMR0IE = 1; /* Enable Timer0 interrupts. */
    INTCONbits.GIEH = 1; /* Make sure high priority interrupts are enabled. */
    INTCONbits.GIEL = 1; /* Make sure low priority interrupts are enabled. */
}

void samplingModeService(void)
{
    uint8_t numBytesRead;

    numBytesRead = getsUSBUSART(readBuffer, 1);
    if (numBytesRead == 1 && readBuffer[0] == 0x00) /* Reset */
    {
        /* Disable Timer0 interrupts. */
        INTCONbits.TMR0IE = 0;
        inSamplingMode = false;
        return;
    }

    INTCONbits.TMR0IE = 0;
    if (pendingPulse > 5)
    {
        writeBuffer[0] = (uint8_t)(pendingPulse / 256);
        writeBuffer[1] = (uint8_t)(pendingPulse % 256);
        putUSBUSART(writeBuffer, 2);
        pendingPulse = 0;
        endOfTransmission = false;
        waitForEOT = true;
    }

    if ((waitForEOT == true) && (endOfTransmission == true))
    {
        writeBuffer[0] = 0xFF;
        writeBuffer[1] = 0xFF;
        putUSBUSART(writeBuffer, 2);
        waitForEOT = false;
    }
    INTCONbits.TMR0IE = 1;
}

void APP_IRToyEmulationInterrupt(void)
{
    if (INTCONbits.TMR0IF == 1)
    {
        /* Check if IR sensor (inverted input) is active. */
        if (PORTCbits.RC7 == 0)
        {
            if (pulseLowLen > 5)
            {
                pendingPulse = pulseLowLen;
            }
            pulseLowLen = 0;

            pulseHighLen += 2;
        }
        else
        {
            if (pulseHighLen > 5)
            {
                pendingPulse = pulseHighLen;
                endOfTransmission = false;
            }
            pulseHighLen = 0;

            if (endOfTransmission == false)
            {
                pulseLowLen += 2;
                if (pulseLowLen >= 0xFFFE)
                {
                    pulseLowLen = 0;
                    endOfTransmission = true;
                }
            }
        }
        INTCONbits.TMR0IF = 0;
    }
}

void APP_IRToyEmulationInitialize()
{   
    line_coding.bCharFormat = 0;
    line_coding.bDataBits = 8;
    line_coding.bParityType = 0;
    line_coding.dwDTERate = 115200;

    /* I/O settings as used in orignal Bit Trade One firmware. */
    TRISB  = 0x00;
    TRISC  = 0x80;
    LATC   = 0xFF;
    ANSEL  = 0x00;
    ANSELH = 0x00;

    /* But turn off IR sender diode output, no support implemented. */
    PORTCbits.RC5 = 0;
}

void APP_IRToyEmulationTasks()
{
    /* If the USB device isn't configured yet, we can't really do anything
     * else since we don't have a host to talk to.  So jump back to the
     * top of the while loop. */
    if (USBGetDeviceState() < CONFIGURED_STATE)
    {
        return;
    }

    /* If we are currently suspended, then we need to see if we need to
     * issue a remote wakeup.  In either case, we shouldn't process any
     * keyboard commands since we aren't currently communicating to the host
     * thus just continue back to the start of the while loop. */
    if (USBIsDeviceSuspended() == true)
    {
        return;
    }

    if (inSamplingMode == true)
    {
        samplingModeService();
    }
    else
    {
        if (USBUSARTIsTxTrfReady() == true)
        {
            uint8_t numBytesRead;
            numBytesRead = getsUSBUSART(readBuffer, 1);

            if (numBytesRead == 1)
            {
                switch (readBuffer[0])
                {
                case 'S': /* IRIO Sampling Mode */
                case 's':
                    writeBuffer[0] = 'S'; /* Answer OK */
                    writeBuffer[1] = '0';
                    writeBuffer[2] = '1';
                    putUSBUSART(writeBuffer, 3);
                    samplingModeSetup();
                    inSamplingMode = true;
                    break;

                case 'V': /* Acquire Version */
                case 'v':
                    writeBuffer[0] = 'V'; /* Answer OK */
                    writeBuffer[1] = '9'; /* Hardware Version */
                    writeBuffer[2] = '9'; /* Firmware Version High */
                    writeBuffer[3] = '9'; /* Firmware Version Low */
                    putUSBUSART(writeBuffer, 4);
                    break;

                default: /* All the rest is unsupported! */
                    writeBuffer[0] = '?';
                    putUSBUSART(writeBuffer, 1);
                    break;
                }
            }
        }
    }

#if defined(USB_POLLING)
    USBDeviceTasks();
#endif
    CDCTxService();
}
          


To use it in LIRC, setup lirc_options.conf similar to the IR Toy using:

driver = irtoy
device = /dev/ttyACM0
          


Topic: Scripts and Code, by Kjetil @ 23/01-2021, Article Link

USB Relay Control Simplified

I got a USB controlled relay board a while ago, which is identified with the USB ID 16c0:05df. There are various controller programs for these on GitHub, but all of them are rather complicated, so I decided to write my own simplified version using the same principles. Like others, it has a dependency against the hidapi library.

The result is this C program:

#include <hidapi/hidapi.h>
#include <wchar.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define TARGET_DEVICE L"USBRelay2"

#define RELAY_OFF 0xfd
#define RELAY_ON  0xff

int main(int argc, char *argv[])
{
  struct hid_device_info *hidinfo;
  hid_device *hiddev;
  unsigned char buf[8];
  int result, relay_number, relay_operation;

  hidinfo = hid_enumerate(0, 0);
  while (hidinfo != NULL) {
    if (0 == wcscmp(hidinfo->product_string, TARGET_DEVICE)) {
      break;
    }
    hidinfo = hidinfo->next;
  }

  if (hidinfo == NULL) {
    fprintf(stderr, "Did not find '%ls' in USB devices!\n", TARGET_DEVICE);
    return 1;
  }

  hiddev = hid_open_path(hidinfo->path);
  if (hiddev == NULL) {
    fprintf(stderr, "hid_open_path() failed on: %s\n", hidinfo->path);
    return 1;
  }

  if (argc > 2) {
    relay_number = atoi(argv[1]) & 0b11; /* Limit to 2 relays. */
    relay_operation = 0;
    if (0 == strcasecmp(argv[2], "on")) {
      relay_operation = RELAY_ON;
    } else if (0 == strcasecmp(argv[2], "off")) {
      relay_operation = RELAY_OFF;
    } else {
      fprintf(stderr, "Invalid relay operation\n");
      hid_close(hiddev);
      return 1;
    }

    memset(buf, 0x00, sizeof(buf));
    buf[0] = 0x00;
    buf[1] = relay_operation;
    buf[2] = relay_number;
    result = hid_write(hiddev, buf, sizeof(buf));
    if (result == -1) {
      fprintf(stderr, "hid_write() failed!\n");
      hid_close(hiddev);
      return 1;
    }
  }

  buf[0] = 0x01;
  result = hid_get_feature_report(hiddev, buf, sizeof(buf));
  if (result == -1) {
    fprintf(stderr, "hid_get_feature_report() failed!\n");
    hid_close(hiddev);
    return 1;
  }

  printf("Relay #1: %d\n",  buf[7] & 0b01);
  printf("Relay #2: %d\n", (buf[7] & 0b10) >> 1);
  printf("Usage: %s <relay number> <on|off>\n", argv[0]);

  hid_close(hiddev);
  return 0;
}
          


Compile it like so: gcc -o usbrelay2 usbrelay2.c -lhidapi-hidraw

Topic: Scripts and Code, by Kjetil @ 02/01-2021, Article Link

Gould 1604 Oscilloscope Repair

I have gotten hold of a Gould DRO 1604 oscilloscope from 1989. It would power on, but the display was severely distorted and looked like this:

Gould 1604 Before


I cracked open the case and first of all removed a leaking battery, which is not related to the display problem, but important to fix as soon as possible:

Gould 1604 Battery


Luckily there is a service manual available for this particular oscilloscope, which helps a lot in giving troubleshooting tips and places to measure. I first discovered that pin 4 of U803 (the Y DAC chip) had -3.3V but the service manual said it should be either -2.8V or -4.2V, depending on the "dot joining" state. This led me to Q811 (a MOSFET controlling the "dot join") which had a strange voltage of -3.41V on it's gate, where it should have been either 0V or -5V. According to the circuit diagram the gate of Q811 is pulled by a resistor connected to the -12V power rail.

I measured the -12V rail, and this was only around -11V which is too low. This rail is controlled by a LM377T linear regulator, which is adjusted by a set of resistors and capacitors. I checked the three associated resistors R1, R2 and R16 and they all had the correct values. However, the two capacitors C15 and C16 had correct capacitance but horrible ESR value of 23 and 33 ohms.

Here are the two bad capacitors, with blue casing:

Gould 1604 Old Capacitors


Which i promptly replaced with new ones:

Gould 1604 New Capacitors


This fixed the -12V power rail, which now measures around actual -12 volts, and the display is now working:

Gould 1604 After


Topic: Repair, by Kjetil @ 04/12-2020, Article Link

Toshiba Satellite Pro 410CDT Tweaks

I got hold of an old Toshiba Satellite Pro 410CDT laptop with a Pentium 90MHz processor, which I have cleaned up and refurbished. Since I already got tons of Linux boxes I figured to use this a "DOS Gaming Laptop" instead. It has a Sound Blaster compatible ESS688 sound chipset and a Adlib compatible FM synthesizer, making this perfect for that use.

Important notice! The internal batteries in this had already started to shown signs of leakage, the typical turquoise spots:

Toshiba 410CDT Batteries

I immediately removed the batteries and cleaned up the spots with vinegar. It will now complain about lost CMOS settings every time, but I can live with that for now.

Another challenge is that this laptop has no floppy drive, since that is swappable with a CD-ROM drive that I (only) got instead. To be able to install DOS I used QEMU to install it on a virtual drive, then removed the original hard drive from the laptop and DD'd over the virtual drive to it.

I knew the hard drive was 815394816 bytes, meaning 1592568 512-byte sectors, so a virtual drive can be made like this:

dd if=/dev/zero of=Toshiba_DOS.dd bs=512 count=1592568
          


QEMU is launched like this:

qemu-system-i386 -drive format=raw,file=Toshiba.dd -cpu pentium -m 32 -monitor stdio -fda DOS_Floppy_1.dd
          


One can then use the QEMU monitor to change and eject virtual floppies like so:

change floppy0 DOS_Floppy_2.dd
change floppy0 DOS_Floppy_3.dd
eject floppy0
          


Afterwards it is also possible to loopback mount he virtual hard drive to put more stuff on there, like tools and games. Since the first partition starts at sector 63, an offset of 32256 bytes must be used:

sudo mount -o loop,offset=32256 Toshiba_DOS.dd /mnt/loop/
          


I used one of those USB-to-IDE adapter and the virtual hard drive is typically DD'd back just like this:

dd if=Toshiba_DOS.dd of=/dev/sdd bs=512 status=progress
          


Finally, for reference, here is the "AUTOEXEC.BAT" file I ended up using for the laptop:

C:\DOS\SMARTDRV.EXE /X
@ECHO OFF
PROMPT $p$g
PATH C:\DOS;C:\VI;C:\MSKERMIT;C:\PKZIP
SET TEMP=C:\DOS
MODE CON CODEPAGE PREPARE=((850) C:\DOS\EGA.CPI)
MODE CON CODEPAGE SELECT=850
LOADHIGH=C:\DOS\KEYB NO,,C:\DOS\KEYBOARD.SYS
LOADHIGH=C:\DOS\DOSKEY.COM
LOADHIGH=C:\DRIVERS\MOUSE.COM
LOADHIGH=C:\DRIVERS\MSCDEX.EXE /D:MSCD001 /L:D
C:\ESSUTIL\ESSVOL.EXE /V:8 /L:8 /W:8 /M:0 /C:8 /S:8
SET BLASTER=A220 I7 D1 T6 P330 H5
          


And the "CONFIG.SYS" file:

DEVICE=C:\DOS\HIMEM.SYS
DEVICE=C:\DOS\EMM386.EXE NOEMS
DOS=HIGH,UMB
COUNTRY=047,,C:\DOS\COUNTRY.SYS
DEVICEHIGH=C:\DOS\SETVER.EXE
DEVICEHIGH=C:\DOS\DISPLAY.SYS CON=(EGA,,1)
FILES=50
BUFFERS=10,0
DEVICEHIGH=C:\DRIVERS\OAKCDROM.SYS /D:MSCD001
          


Topic: Configuration, by Kjetil @ 15/11-2020, Article Link

Compaq Deskpro XL 5133 with Red Hat 5.2

I decided to install the classic Red Hat Linux 5.2 distribution on my classic Compaq Deskpro XL 5133 machine. The 5.2 version is one of the more well known from the late 90's, and several others have used this to experience the past. It is using the 2.0.36 version of the Linux kernel.

Before any of the SW installation could take place, the on-board battery had to be changed to be able to keep the system configuration intact. Luckily the battery is a Lithium type, so it doesn't leak, but it was soldered in place. I changed it with a CR2032 battery holder, which works fine.

CR2032 battery holder replacement


After configuring the system with the special Compaq floppy disks (there is no BIOS setup menu!) I was able to install Red Hat 5.2 using the CD-ROM without any trouble. The machine has a Matrox Millennium VGA card which works fine in X Windows and a on-board AMD PCnet32 Ethernet controller working out of the box.

The troublesome part was getting the audio to work, which is classified as "Compaq Deskpro XL Business Audio", but is in reality a "Microsoft Sound System" compatible chip of the AD1847 type:

AD1847JP SoundPort Chip


When playing any audio, it would stutter and the following error would appear:

Sound: DMA (output) timed out - IRQ/DRQ config error?
          

I tried all kinds of different IRQ and DMA settings, but to no avail. To troubleshoot further I setup a QEMU emulated environment also with Red Hat 5.2 to be able to quickly recompile the ad1848.o module device driver.

I figured out that in vanilla Linux 2.0.36 the sound drivers are not modularized, and Red Hat had actually applied a patch to modularize them. So this exact setup had to be re-recreated. The original sources can be found here as "kernel-2.0.36-0.7.src.rpm". But these still needs to be patched, where I did the following:

tar -xvzf linux-2.0.35.tar.gz
gunzip 2.0.36-pre-patch-14.gz
gunzip sound.diff.gz
patch -p0 < 2.0.36-pre-patch-14
patch -p0 < sound.diff
mv linux linux-2.0.36
patch -p0 < kernel-2.0.36-sound-new.patch
cp kernel-2.0.36-i386.config linux-2.0.36/.config
          

Yes, the original sources is actually Linux 2.0.35, but with a patch to bump it up to 2.0.36!

After enabling debugging flags, I eventually found out that this stock driver is detecting the audio chip wrongly as a "OPTi 82C930" chip, which in turn causes the IRQ status to be read from the wrong register!

Here is my own patch to fix this problem and enabling the debug:

--- ad1848.c.orig    2020-08-30 12:42:45.362175159 +0200
+++ ad1848.c    2020-08-30 12:42:52.142175232 +0200
@@ -37,6 +37,9 @@
 
 #include "soundmodule.h"
 
+#define DEBUGXL
+#define DDB
+
 #define DEB(x)
 #define DEB1(x)
 #include "sound_config.h"
@@ -1532,10 +1535,19 @@
     {
         if ((tmp1 = ad_read(devc, i)) != (tmp2 = ad_read(devc, i + 16)))
         {
-            DDB(printk("ad1848 detect step F(%d/%x/%x) - OPTi chip???\n", i, tmp1, tmp2));
-            if (!ad1847_flag)
-                optiC930 = 1;
-            break;
+            if (deskpro_xl)
+            {
+                DDB(printk("Deskpro XL, so assuming AD1847\n"));
+                ad1847_flag = 1;
+                break;
+            }
+            else
+            {
+                DDB(printk("ad1848 detect step F(%d/%x/%x) - OPTi chip???\n", i, tmp1, tmp2));
+                if (!ad1847_flag)
+                    optiC930 = 1;
+                break;
+            }
         }
     }
 
@@ -1688,7 +1700,10 @@
                         }
                         else
                         {
-                            devc->model = MD_4231;
+                            if (! deskpro_xl)
+                            {
+                                devc->model = MD_4231;
+                            }
                         }
                 }
             }
@@ -1708,6 +1723,7 @@
     if (devc->model == MD_1848 && ad1847_flag)
         devc->chip_name = "AD1847";
 
+    DDB(printk("ad1848_detect() - '%s' (%d)\n", devc->chip_name, devc->model));
 
     return 1;
 }
          

Or you can download my recompiled version here.

The /etc/conf.modules section ended up being like this for the driver:

alias sound ad1848
alias midi opl3
options opl3 io=0x388
options ad1848 io=0x530 irq=9 dma=1,0 type=2 deskpro_xl=1
          


Compaq Deskpro XL 5133


Topic: Configuration, by Kjetil @ 01/11-2020, Article Link

Amitech Amiga Mouse Repair

I have an Amiga mouse that behaved so poorly that it was unusable. The symptom was that movement was barely detected, if at all at some times. After digging around on the Internet I found that one possible cause is the IR emitter and IR photo-transistors that are used to generate the quadrature encoder pulses.

I ordered replacements from my local dealer, specifically:
* OP 750A - IR phototransistor 850 nm 30 V Side Looking, Optek

IR phototransistors


* IRL 81A - IR emitter 860nm 100mA 1.3V THT, Osram Opto Semiconductors

IR emitters


And soldered on these replacements:

Amitech mouse replacements


This is indeed what was causing the problems and the mouse is now working again:

Amitech Denmark Logo


Topic: Repair, by Kjetil @ 03/10-2020, Article Link

Linux Distribution for LOADLIN

This is a similar project to the Linux Distribution for 386SX but this with some different goals. Most importantly to boot it with LOADLIN directly from DOS and keeping the root filesystem in RAM using Cramfs. In addition, I wanted to have functioning SLIP support.

I ended up using these specific software versions:
* linux-2.4.37.11
* gcc-3.4.6
* busybox-1.19.4
* uClibc-0.9.33.2
* binutils-2.32

Get the necessary scripts, configuration and patches here to make it yourself. Or just get the completed kernel and root filesystem here.

For easy reference, here is the script to compile everything:

#!/bin/bash
set -e

TARGET="i386-linux-uclibc"
PREFIX="${HOME}/opt/gcc-${TARGET}/"
SYSROOT="${PREFIX}/${TARGET}/sysroot"

GCC_SRC="gcc-3.4.6.tar.bz2"
BINUTILS_SRC="binutils-2.32.tar.xz"
UCLIBC_SRC="uClibc-0.9.33.2.tar.xz"
LINUX_SRC="linux-2.4.37.11.tar.xz"
BUSYBOX_SRC="busybox-1.19.4.tar.bz2"

export PATH="${PREFIX}bin:$PATH"

# Prepare Prefix and System Root
if [ -d "$SYSROOT" ]; then
  echo "Old system root directory detected, please remove it."
  exit 1
else
  mkdir -p "$SYSROOT/usr"
fi

# Prepare Build Directories:
if [ -d build ]; then
  echo "Old build directory detected, please remove it."
  exit 1
else
  mkdir -p build/binutils
  mkdir -p build/gcc-stage1
  mkdir -p build/gcc-stage2
  mkdir -p build/uclibc
  mkdir -p build/linux
  mkdir -p build/busybox
fi

# Unpack Sources:
if [ -d source ]; then
  cd source
  tar -xvjf "$GCC_SRC"
  tar -xvJf "$BINUTILS_SRC"
  tar -xvJf "$UCLIBC_SRC" -C ../build/uclibc
  tar -xvJf "$LINUX_SRC" -C ../build/linux
  tar -xvjf "$BUSYBOX_SRC" -C ../build/busybox
  cd -
else
  echo "No source directory, please download sources."
  exit 1
fi

# Patch gcc-3.4.6:
cd "source/gcc-3.4.6/gcc/config/i386/"
if ! fgrep --silent "inhibit_libc" linux.h; then
  patch -p 0 < ../../../../../gcc-3.4.6-linux.h.patch
fi
cd -

# Patch linux-2.4.37.11:
cd "build/linux/linux-2.4.37.11/include/linux/"
if ! fgrep --silent "<linux/types.h>" filter.h; then
  patch -p 0 < ../../../../../linux-2.4.37.11-filter.h.patch
fi
cd -

# Install Linux 2.4 Headers:
cd build/linux/linux-*
make ARCH=i386 mrproper
make ARCH=i386 include/linux/version.h
make ARCH=i386 symlinks
mkdir -p "$SYSROOT/usr/include/asm"
cp -v -R -H include/asm "$SYSROOT/usr/include"
cp -v -R include/asm-generic "$SYSROOT/usr/include"
cp -v -R include/linux "$SYSROOT/usr/include"
touch "${SYSROOT}/usr/include/linux/autoconf.h"
cd -

# Build binutils:
cd build/binutils
../../source/binutils-*/configure --target="$TARGET" --prefix="$PREFIX" --with-sysroot="$SYSROOT" --disable-werror --enable-languages=c,c++ --enable-shared --without-newlib --disable-libgomp --enable-fast-install=N/A
make all-{binutils,gas,ld}
make install-{binutils,ld,gas}
cd -

# Build Stage 1 GCC3:
cd build/gcc-stage1
../../source/gcc-3*/configure --target="$TARGET" --prefix="$PREFIX" --with-sysroot="$SYSROOT" --with-cpu=i386 --disable-fast-install --disable-werror --disable-multilib --enable-languages=c --without-headers --disable-shared --disable-libssp --disable-libmudflap --with-newlib --disable-c99 --disable-libgomp --disable-threads
make all-gcc
make install-gcc
cd -

# Install uClibc Headers:
cd build/uclibc/uClibc-*
cp -v ../../../config-uclibc .config
sed -i -e "s%KERNEL_HEADERS=.*%KERNEL_HEADERS=\"$SYSROOT/usr/include/\"%" .config
make ARCH=i386 PREFIX="$SYSROOT" install_headers
cd -

# Build uClibc:
cd build/uclibc/uClibc-*
make ARCH=i386 PREFIX="$SYSROOT"
make ARCH=i386 PREFIX="$SYSROOT" install
cd -

# Build Stage 2 GCC3:
cd build/gcc-stage2
../../source/gcc-3*/configure --target="$TARGET" --prefix="$PREFIX" --with-sysroot="$SYSROOT" --with-cpu=i386 --enable-fast-install=N/A --disable-werror --enable-languages=c,c++ --disable-shared --without-newlib --disable-libgomp --disable-threads
make all-gcc
make install-gcc
cd -

# Build Linux 2.4:
cd build/linux/linux-*
cp -v ../../../config-linux .config
make ARCH=i386 CROSS_COMPILE=i386-linux-uclibc- oldconfig
make ARCH=i386 CROSS_COMPILE=i386-linux-uclibc- dep
make ARCH=i386 CROSS_COMPILE=i386-linux-uclibc- bzImage
cd -

# Build Busybox:
cd build/busybox/busybox-*
cp -v ../../../config-busybox .config
make CROSS_COMPILE=i386-linux-uclibc-
cd -
          


And here is the script to make the root filesystem:

#!/bin/bash
set -e

ROOTFS="`pwd`/rootfs/"

TARGET="i386-linux-uclibc"
PREFIX="${HOME}/opt/gcc-${TARGET}/"
SYSROOT="${PREFIX}/${TARGET}/sysroot"

export PATH="${PREFIX}bin:$PATH"

if [ -d "$ROOTFS" ]; then
  echo "Old root FS directory detected, please remove it."
  exit 1
fi
mkdir -p "$ROOTFS"

# Install Busybox:
cd build/busybox/busybox-*
make CROSS_COMPILE=i386-linux-uclibc- CONFIG_PREFIX="$ROOTFS" install
cd -

# Create some essential directories
cd "$ROOTFS"
mkdir etc
mkdir etc/init.d
mkdir lib
mkdir proc
mkdir sys
mkdir tmp
mkdir root
mkdir dev
mkdir dev/pts
cd -

# Initial rc.S:
cat > rcS <<EOF
#!/bin/sh
mount -t proc /proc /proc
mount -t devpts /dev/pts /dev/pts
mount -t tmpfs /tmp /tmp
loadkmap < /etc/no-latin1.bmap
hostname busybox
EOF
mv -v rcS "$ROOTFS/etc/init.d/"

# Initial inittab:
cat > inittab <<EOF
::sysinit:/etc/init.d/rcS
::respawn:-/bin/sh
::ctrlaltdel:/sbin/reboot
::shutdown:/bin/umount -a -r
::restart:/sbin/init
EOF
mv -v inittab "$ROOTFS/etc/"

# Copy this system's keymap:
loadkeys -b /usr/share/kbd/keymaps/i386/qwerty/no-latin1.map.gz > "$ROOTFS/etc/no-latin1.bmap"

# Make everything root user:
sudo chown -R root:root "$ROOTFS"

# Create some critical devices:
sudo mknod "$ROOTFS/dev/tty" c 5 0
sudo mknod "$ROOTFS/dev/console" c 5 1
sudo mknod -m 0666 "$ROOTFS/dev/null" c 1 3

# Create some useful devices:
sudo mknod "$ROOTFS/dev/rtc" c 10 135
sudo mknod "$ROOTFS/dev/tty0" c 4 0
sudo mknod "$ROOTFS/dev/tty1" c 4 1
sudo mknod "$ROOTFS/dev/tty2" c 4 2
sudo mknod "$ROOTFS/dev/tty3" c 4 3
sudo mknod "$ROOTFS/dev/ttyS0" c 4 64
sudo mknod "$ROOTFS/dev/ttyS1" c 4 65
sudo mknod "$ROOTFS/dev/fd0" b 2 0
sudo mknod "$ROOTFS/dev/fd1" b 2 1
sudo mknod "$ROOTFS/dev/root" b 4 0
sudo mknod "$ROOTFS/dev/lp0" c 6 0

# SetUID on busybox binary:
sudo chmod +s "$ROOTFS/bin/busybox"

# Make rcS executable:
sudo chmod +x "$ROOTFS/etc/init.d/rcS"

# Make Compressed ROM archive:
mkfs.cramfs rootfs rootfs.cramfs
          


Instead of using LOADLIN, it is actually easy to start this with QEMU as well, like so:

qemu-system-i386 -kernel bzImage -initrd rootfs.cramfs
          


Topic: Configuration, by Kjetil @ 12/09-2020, Article Link

Outlaws in Wine

I have made an effort in getting the classic LucasArts game Outlaws working in Wine with music, which is essential due to its excellent soundtrack. This is a similar to my other effort with M.I.A., except this time the music playback mechanism is more advanced. I had to make additional hacks to the Wine "mcicda" library to make it pause and resume in the middle of tracks.

Here is a rough guide for the commands required for installation. Also, when the installation asks about DirectX 3.0A, just skip this.

mkdir -p ~/opt/outlaws
mkdir ~/opt/outlaws/cd1 # Then copy files from CD1 into here.
mkdir ~/opt/outlaws/cd2 # Then copy files from CD2 into here.
echo "OUTLAWS_1" > ~/opt/outlaws/cd1/.windows-label
echo "OUTLAWS_2" > ~/opt/outlaws/cd2/.windows-label
ln -s "cd1" ~/opt/outlaws/drive_d
WINEARCH=win32 WINEPREFIX=~/opt/outlaws winecfg # D: = "drive_d" = CD-ROM
WINEARCH=win32 WINEPREFIX=~/opt/outlaws wine ~/opt/outlaws/drive_d/SETUP.EXE
          


I had to enable the "Virtual Desktop" setting with "winecfg" for certain stuff like menus to work. I also just configured my X windows resolution to 800x600 before playing the game.

To make the music playback work, a lot of additional steps are required. First the patched "mcicda.dll", which can be downloaded here. This should placed at "~/opt/outlaws/drive_c/windows/system32/mcicda.dll"

Here is the patched code in case you want to compile it yourself, using the Wine 4.0.2 source code as a basis:

--- mcicda.c.orig	2020-08-22 14:22:13.861217377 +0200
+++ mcicda.c	2020-08-22 14:22:18.323217425 +0200
@@ -20,11 +20,21 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
  */
 
+#define MPLAYER_FIFO_LOCATION "/tmp/mplayer.fifo"
+#define TOC_FILE_LOCATION "/tmp/toc.txt"
+
 #include "config.h"
 #include <stdarg.h>
 #include <stdio.h>
 #include <string.h>
 
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <limits.h>
+#include <time.h>
+
 #define WIN32_NO_STATUS
 #include "windef.h"
 #include "winbase.h"
@@ -79,10 +89,211 @@
 typedef HRESULT(WINAPI*LPDIRECTSOUNDCREATE)(LPCGUID,LPDIRECTSOUND*,LPUNKNOWN);
 static LPDIRECTSOUNDCREATE pDirectSoundCreate;
 
+static int mplayer_current_track = 0;
+static int mplayer_current_min   = 0;
+static int mplayer_current_sec   = 0;
+static int mplayer_current_frame = 0;
+static struct timespec mplayer_playback_started = {0,0};
+
+static void mplayer_command(const char *command)
+{
+    int fd, written;
+
+    fd = open(MPLAYER_FIFO_LOCATION, O_NONBLOCK | O_WRONLY);
+    if (fd == -1) {
+        TRACE("No pipe\n");
+        return;
+    }
+
+    written = write(fd, command, strlen(command));
+    if (written <= 0) {
+        TRACE("Write failed\n");
+    }
+
+    close(fd);
+}
+
 static BOOL device_io(HANDLE dev, DWORD code, void *inbuffer, DWORD insize, void *outbuffer, DWORD outsize, DWORD *retsize, OVERLAPPED *overlapped)
 {
     const char *str;
-    BOOL ret = DeviceIoControl(dev, code, inbuffer, insize, outbuffer, outsize, retsize, overlapped);
+
+    int track_no, min, sec, frame;
+    BOOL ret = TRUE;
+    CDROM_TOC *toc;
+    CDROM_SUB_Q_DATA_FORMAT *qfmt;
+    SUB_Q_CHANNEL_DATA *qdata;
+    FILE *fh;
+    char buf[16];
+    struct timespec now;
+
+    *retsize = 0;
+
+    switch (code) {
+    case IOCTL_CDROM_READ_TOC:
+        toc = (CDROM_TOC *)outbuffer;
+        *retsize = CDROM_TOC_SIZE;
+
+        toc->Length[0] = 0;
+        toc->Length[1] = 0;
+        toc->FirstTrack = 1;
+        toc->LastTrack = 1;
+
+        // Set up first track as data track.
+        toc->TrackData[0].TrackNumber = 1;
+        toc->TrackData[0].Control = 0x4;
+        toc->TrackData[0].Address[1] = 0;
+        toc->TrackData[0].Address[2] = 0;
+        toc->TrackData[0].Address[3] = 0;
+
+        // Get other audio tracks from toc file.
+        fh = fopen(TOC_FILE_LOCATION, "r");
+        if (fh == NULL) {
+          TRACE("IOCTL_CDROM_READ_TOC, Failed to open: %s\n", TOC_FILE_LOCATION);
+          break;
+        }
+
+        while (fgets(buf, sizeof(buf), fh) != NULL)
+        {
+          sscanf(buf, "%02d:%02d:%02d", &min, &sec, &frame);
+
+          toc->TrackData[toc->LastTrack].TrackNumber = toc->LastTrack + 1;
+          toc->TrackData[toc->LastTrack].Control = 0;
+          toc->TrackData[toc->LastTrack].Address[1] = min;
+          toc->TrackData[toc->LastTrack].Address[2] = sec;
+          toc->TrackData[toc->LastTrack].Address[3] = frame;
+
+          toc->LastTrack++;
+
+          TRACE("IOCTL_CDROM_READ_TOC, Track %d = %02d:%02d:%02d\n", toc->LastTrack, min, sec, frame);
+        }
+
+        toc->LastTrack--; // Remove the last dummy track:
+
+        fclose(fh);
+        break;
+
+    case IOCTL_CDROM_STOP_AUDIO:
+        mplayer_command("stop\n");
+        break;
+
+    case IOCTL_CDROM_PAUSE_AUDIO:
+        mplayer_command("stop\n");
+        break;
+
+    case IOCTL_CDROM_READ_Q_CHANNEL:
+        qfmt  = (CDROM_SUB_Q_DATA_FORMAT *)inbuffer;
+        qdata = (SUB_Q_CHANNEL_DATA *)outbuffer;
+        *retsize = sizeof(SUB_Q_CHANNEL_DATA);
+
+        if (qfmt->Format == IOCTL_CDROM_CURRENT_POSITION)
+        {
+            qdata->CurrentPosition.FormatCode = IOCTL_CDROM_CURRENT_POSITION;
+            qdata->CurrentPosition.Control = 0;
+            qdata->CurrentPosition.ADR = 0;
+            qdata->CurrentPosition.TrackNumber = mplayer_current_track;
+            qdata->CurrentPosition.IndexNumber = 0;
+
+            clock_gettime(CLOCK_MONOTONIC, &now);
+            TRACE("IOCTL_CDROM_READ_Q_CHANNEL, Started = %lu.%lu\n",
+                mplayer_playback_started.tv_sec, mplayer_playback_started.tv_nsec);
+            TRACE("IOCTL_CDROM_READ_Q_CHANNEL, Now = %lu.%lu\n", now.tv_sec, now.tv_nsec);
+
+            min = (now.tv_sec - mplayer_playback_started.tv_sec) / 60;
+            sec = (now.tv_sec - mplayer_playback_started.tv_sec) % 60;
+            frame = (now.tv_nsec - mplayer_playback_started.tv_nsec);
+            if (frame < 0) {
+                frame = 0 - frame;
+                sec--;
+                if (sec < 0) {
+                  sec = 59;
+                  min--;
+                }
+            }
+            frame = (frame / 10000000) * 0.75;
+
+            if (mplayer_current_min > 0 ||
+                mplayer_current_sec > 0 ||
+                mplayer_current_frame > 0)
+            {
+                min += mplayer_current_min;
+                sec += mplayer_current_sec;
+                if (sec >= 60) {
+                    sec -= 60;
+                    min++;
+                }
+                frame += mplayer_current_frame;
+                if (frame >= 76) {
+                    frame -= 76;
+                    sec++;
+                    if (sec >= 60) {
+                        sec -= 60;
+                        min++;
+                    }
+                }
+            }
+
+            qdata->CurrentPosition.TrackRelativeAddress[0] = 0;
+            qdata->CurrentPosition.TrackRelativeAddress[1] = min;
+            qdata->CurrentPosition.TrackRelativeAddress[2] = sec;
+            qdata->CurrentPosition.TrackRelativeAddress[3] = frame;
+
+            qdata->CurrentPosition.AbsoluteAddress[0] = 0;
+            qdata->CurrentPosition.AbsoluteAddress[1] = min;
+            qdata->CurrentPosition.AbsoluteAddress[2] = sec;
+            qdata->CurrentPosition.AbsoluteAddress[3] = frame;
+
+            fh = fopen(TOC_FILE_LOCATION, "r");
+            if (fh == NULL) {
+              TRACE("IOCTL_CDROM_READ_Q_CHANNEL, Failed to open: %s\n", TOC_FILE_LOCATION);
+              break;
+            }
+
+            track_no = 2;
+            while (fgets(buf, sizeof(buf), fh) != NULL)
+            {
+                if (track_no == mplayer_current_track) {
+                    sscanf(buf, "%02d:%02d:%02d", &min, &sec, &frame);
+                    qdata->CurrentPosition.AbsoluteAddress[1] += min;
+                    qdata->CurrentPosition.AbsoluteAddress[2] += sec;
+                    if (qdata->CurrentPosition.AbsoluteAddress[2] >= 60) {
+                        qdata->CurrentPosition.AbsoluteAddress[2] -= 60;
+                        qdata->CurrentPosition.AbsoluteAddress[1]++;
+                    }
+                    qdata->CurrentPosition.AbsoluteAddress[3] += frame;
+                    if (qdata->CurrentPosition.AbsoluteAddress[3] >= 76) {
+                        qdata->CurrentPosition.AbsoluteAddress[3] -= 76;
+                        qdata->CurrentPosition.AbsoluteAddress[2]++;
+                        if (qdata->CurrentPosition.AbsoluteAddress[2] >= 60) {
+                            qdata->CurrentPosition.AbsoluteAddress[2] -= 60;
+                            qdata->CurrentPosition.AbsoluteAddress[1]++;
+                        }
+                    }
+                    break;
+                }
+                track_no++;
+            }
+
+            fclose(fh);
+
+            TRACE("IOCTL_CDROM_READ_Q_CHANNEL, Current Track = %d\n", mplayer_current_track);
+            TRACE("IOCTL_CDROM_READ_Q_CHANNEL, Rel Pos = %02d:%02d:%02d\n",
+                qdata->CurrentPosition.TrackRelativeAddress[1],
+                qdata->CurrentPosition.TrackRelativeAddress[2],
+                qdata->CurrentPosition.TrackRelativeAddress[3]);
+            TRACE("IOCTL_CDROM_READ_Q_CHANNEL, Abs Pos = %02d:%02d:%02d\n",
+                qdata->CurrentPosition.AbsoluteAddress[1],
+                qdata->CurrentPosition.AbsoluteAddress[2],
+                qdata->CurrentPosition.AbsoluteAddress[3]);
+        }
+        else
+        {
+           TRACE("IOCTL_CDROM_READ_Q_CHANNEL, Unknown format: %d\n", qfmt->Format);
+        }
+        break;
+
+    default:
+        break;
+    }
 
 #define XX(x) case (x): str = #x; break
     switch (code)
@@ -906,6 +1117,9 @@
     SUB_Q_CHANNEL_DATA          data;
     CDROM_TOC			toc;
 
+    int track_no, min, sec, frame;
+    char command[PATH_MAX];
+
     TRACE("(%04X, %08X, %p);\n", wDevID, dwFlags, lpParms);
 
     if (lpParms == NULL)
@@ -914,6 +1128,35 @@
     if (wmcda == NULL)
 	return MCIERR_INVALID_DEVICE_ID;
 
+    mplayer_command("stop\n");
+
+    track_no = MCI_TMSF_TRACK(lpParms->dwFrom);
+    min      = MCI_TMSF_MINUTE(lpParms->dwFrom);
+    sec      = MCI_TMSF_SECOND(lpParms->dwFrom);
+    frame    = MCI_TMSF_FRAME(lpParms->dwFrom);
+    TRACE("Track no: %d (%02d:%02d:%02d)\n", track_no, min, sec, frame);
+
+    snprintf(command, PATH_MAX, "loadfile track%02d.flac\n", track_no);
+    mplayer_command(command);
+
+    mplayer_current_track = track_no;
+    clock_gettime(CLOCK_MONOTONIC, &mplayer_playback_started);
+
+    if (min > 0 || sec > 0 || frame > 0) {
+      mplayer_current_min = min;
+      mplayer_current_sec = sec;
+      mplayer_current_frame = frame;
+
+      sec += (min * 60);
+      frame *= 1.333333;
+      TRACE("Seek to: %d.%02d\n", sec, frame);
+
+      snprintf(command, PATH_MAX, "seek %d.%02d 2\n", sec, frame);
+      mplayer_command(command);
+    }
+
+    return 0; // Because of hijacking, this ends here.
+
     if (!MCICDA_ReadTOC(wmcda, &toc, &br))
         return MCICDA_GetError(wmcda);
          


To make music playback work as painlessly as possible, I have once again made a script to start the game:

#!/bin/sh
export WINEPREFIX=~/opt/outlaws

MPLAYER_FIFO=/tmp/mplayer.fifo
TOC_FILE=/tmp/toc.txt
MPLAYER_PID_FILE=/tmp/mplayer.pid

function mplayer_stop {
  if [ -f "$MPLAYER_PID_FILE" ]; then
    kill `cat "$MPLAYER_PID_FILE"`
    rm -f "$MPLAYER_PID_FILE"
  fi
  rm -f "$MPLAYER_FIFO"
}

function mplayer_start {
  if [ ! -p "$MPLAYER_FIFO" ]; then
    mkfifo "$MPLAYER_FIFO"
  fi
  cd "${WINEPREFIX}/drive_d"
  mplayer -vo null -idle -slave -input file=$MPLAYER_FIFO 1&>/dev/null &
  echo "$!" > "$MPLAYER_PID_FILE"
  cd -
}

CD_NO="0"
while [ "$CD_NO" != "1" ] && [ "$CD_NO" != "2" ]; do
  read -p "CD number? [1 or 2] " CD_NO
done

MODE="0"
while [ "$MODE" != "g" ] && [ "$MODE" != "c" ]; do
  read -p "Start [g]ame or [c]hange CD only? " MODE
done

mplayer_stop

rm -f "${WINEPREFIX}/drive_d"
ln -s "cd$CD_NO" "${WINEPREFIX}/drive_d"
cp "${WINEPREFIX}/drive_d/toc.txt" "$TOC_FILE"

mplayer_start

if [ "$MODE" == "c" ]; then
  # If only changing CD, exit now.
  exit 0
fi

(cd "${WINEPREFIX}/drive_c/Program Files/LucasArts/Outlaws" && WINEARCH=win32 wine OLWIN.EXE)

mplayer_stop
rm -f "$TOC_FILE"
          

The same script is also used to change the CD while the game is running, as this is required in some instances.

The CD audio tracks should be ripped to FLAC format and be placed as track02.flac to track08.flac in "~/opt/outlaws/cd1/" for CD1, followed by track02.flac to track09.flac in "~/opt/outlaws/cd2/" for CD2. In addition, you will need a couple of "Table of Contents" files for each CD. These will inform the patched "mcicda.dll" file about the length of each track.

Put the following in "~/opt/outlaws/cd1/toc.txt"

00:00:00
03:16:64
07:34:64
11:23:01
15:13:33
19:58:24
23:39:66
27:15:61
          


Put the following in "~/opt/outlaws/cd2/toc.txt"

00:00:00
04:42:15
10:16:03
13:41:47
20:55:08
24:58:10
30:09:45
33:50:23
36:16:17
          


Enjoy!

Topic: Configuration, by Kjetil @ 24/08-2020, Article Link

Older articles

Newer articles